klastera 1.5.3 → 1.5.5

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 (66) hide show
  1. checksums.yaml +4 -4
  2. data/MIT-LICENSE +20 -0
  3. data/README.rdoc +3 -0
  4. data/Rakefile +37 -0
  5. data/app/assets/javascripts/klastera/application.js +13 -0
  6. data/app/assets/javascripts/klastera/clusters.js +2 -0
  7. data/app/assets/stylesheets/klastera/clusters.scss +46 -0
  8. data/app/controllers/klastera/application_controller.rb +29 -0
  9. data/app/controllers/klastera/clusters_controller.rb +60 -0
  10. data/app/helpers/klastera/application_helper.rb +40 -0
  11. data/app/models/klastera/cluster.rb +5 -0
  12. data/app/models/klastera/cluster_entity.rb +5 -0
  13. data/app/models/klastera/cluster_filter.rb +5 -0
  14. data/app/models/klastera/cluster_user.rb +5 -0
  15. data/app/models/klastera/concerns/cluster.rb +67 -0
  16. data/app/models/klastera/concerns/cluster_entity.rb +18 -0
  17. data/app/models/klastera/concerns/cluster_filter.rb +22 -0
  18. data/app/models/klastera/concerns/cluster_user.rb +41 -0
  19. data/app/models/klastera/concerns/clusterizable.rb +115 -0
  20. data/app/models/klastera/concerns/organization.rb +49 -0
  21. data/app/models/klastera/concerns/transfer.rb +45 -0
  22. data/app/models/klastera/concerns/user.rb +77 -0
  23. data/app/models/klastera/transfer.rb +5 -0
  24. data/app/views/klastera/clusters/_filter.html.erb +0 -0
  25. data/app/views/klastera/clusters/_form.html.erb +22 -0
  26. data/app/views/klastera/clusters/_form_transfer.html.erb +34 -0
  27. data/app/views/klastera/clusters/_table.html.erb +33 -0
  28. data/app/views/klastera/clusters/create.js.erb +1 -0
  29. data/app/views/klastera/clusters/destroy.js.erb +6 -0
  30. data/app/views/klastera/clusters/edit.html.erb +10 -0
  31. data/app/views/klastera/clusters/index.html.erb +21 -0
  32. data/app/views/klastera/clusters/new.html.erb +10 -0
  33. data/app/views/klastera/clusters/transfer.html.erb +10 -0
  34. data/app/views/klastera/clusters/update.js.erb +1 -0
  35. data/app/views/layouts/klastera/_cluster_entity_fields.html.erb +10 -0
  36. data/app/views/layouts/klastera/_cluster_filter.html.erb +23 -0
  37. data/app/views/layouts/klastera/_cluster_role.html.erb +48 -0
  38. data/app/views/layouts/klastera/_cluster_selector.html.erb +21 -0
  39. data/app/views/layouts/klastera/_cluster_user_fields.html.erb +10 -0
  40. data/app/views/layouts/klastera/_nested_cluster_entity.html.erb +43 -0
  41. data/app/views/layouts/klastera/_nested_cluster_user.html.erb +22 -0
  42. data/app/views/layouts/klastera/_options.html.erb +21 -0
  43. data/config/locales/es.yml +96 -0
  44. data/config/routes.rb +7 -0
  45. data/db/migrate/20200324203929_create_klastera_clusters.rb +12 -0
  46. data/db/migrate/20200326111219_add_cluster_options_to_organizations.rb +6 -0
  47. data/db/migrate/20200330010551_create_klastera_cluster_users.rb +9 -0
  48. data/db/migrate/20200330221601_add_order_field_to_clusters.rb +5 -0
  49. data/db/migrate/20200518142609_create_klastera_cluster_entities.rb +8 -0
  50. data/db/migrate/20200908180057_add_cluster_config_to_organization.rb +5 -0
  51. data/db/migrate/20220602222332_add_unique_index_to_cluster_entities.rb +5 -0
  52. data/db/migrate/20250429110829_add_entity_id_index_to_cluster_entities.rb +6 -0
  53. data/db/migrate/20250429110830_add_entity_id_index_and_entity_type_index_to_cluster_entities.klastera.rb +7 -0
  54. data/lib/klastera/engine.rb +5 -0
  55. data/lib/klastera/version.rb +3 -0
  56. data/lib/klastera.rb +253 -0
  57. data/lib/tasks/klastera_tasks.rake +32 -0
  58. data/test/controllers/klastera/clusters_controller_test.rb +52 -0
  59. data/test/fixtures/klastera/cluster_users.yml +9 -0
  60. data/test/fixtures/klastera/clusters.yml +11 -0
  61. data/test/integration/navigation_test.rb +8 -0
  62. data/test/klastera_test.rb +7 -0
  63. data/test/models/klastera/cluster_test.rb +9 -0
  64. data/test/models/klastera/cluster_user_test.rb +9 -0
  65. data/test/test_helper.rb +21 -0
  66. metadata +79 -7
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 26024cfbeac47db7de270c9281e98d197b377f53c4122fb4a6eec246dae615cb
4
- data.tar.gz: f452de2debec0cc4c34549ec98f5dcd387cbb8d32102ed6924ff06ccc89b90cd
3
+ metadata.gz: 1f9a474c54506a3e2a3b92ce5961de94a71dd28699354b671be08337b4bdfac5
4
+ data.tar.gz: 01db2dd1040338587462593e22ef1103f3c7ecd6ce13e48656dcd47eb7cf31c9
5
5
  SHA512:
6
- metadata.gz: '096e198a76a0d1f12b83339d76d9fc6f83c64ac3b893b5dafc5ef1b77f4179aee49b146e1be92d2f9ae9b572c4d108dbc3bb9ba80c78a6404459850a1dcd4265'
7
- data.tar.gz: 416ef7b6a278be2686cf613b5ea5382bd728a480486217f2a2b3511677d93b4631a0818cc08a1a94f74bcf7bc57c7598a3f871a901f14f116b817f8b4ec1dcfe
6
+ metadata.gz: a54cf22f075c58094950f74345fe924686f79ab8454ca6afc4b80555bbe26367d56500867b1fbf83f3f270d9f0effe9d9907ceacdbc18f02f32f35654abf237d
7
+ data.tar.gz: d197f19bc4c590359364cbd965295fb981dedfc22f0e33cf02673c75bfc5228b50f6543d316ee2de887437deec92ceb65bcadc617f1d6a4b1af3f2e44f53ae05
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2020 Gino Barahona
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,3 @@
1
+ = Klastera
2
+
3
+ This project rocks and uses MIT-LICENSE.
data/Rakefile ADDED
@@ -0,0 +1,37 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'Klastera'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.rdoc')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+ APP_RAKEFILE = File.expand_path("../test/dummy/Rakefile", __FILE__)
18
+ load 'rails/tasks/engine.rake'
19
+
20
+
21
+ load 'rails/tasks/statistics.rake'
22
+
23
+
24
+
25
+ Bundler::GemHelper.install_tasks
26
+
27
+ require 'rake/testtask'
28
+
29
+ Rake::TestTask.new(:test) do |t|
30
+ t.libs << 'lib'
31
+ t.libs << 'test'
32
+ t.pattern = 'test/**/*_test.rb'
33
+ t.verbose = false
34
+ end
35
+
36
+
37
+ task default: :test
@@ -0,0 +1,13 @@
1
+ // This is a manifest file that'll be compiled into application.js, which will include all the files
2
+ // listed below.
3
+ //
4
+ // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
5
+ // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
6
+ //
7
+ // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
8
+ // compiled file.
9
+ //
10
+ // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
11
+ // about supported directives.
12
+ //
13
+ //= require_tree .
@@ -0,0 +1,2 @@
1
+ // Place all the behaviors and hooks related to the matching controller here.
2
+ // All this logic will automatically be available in application.js.
@@ -0,0 +1,46 @@
1
+ .nested-cluster-user,
2
+ .nested-cluster-entity {
3
+ .panel {
4
+ margin: 0;
5
+ border-bottom: 0;
6
+ .panel-heading {
7
+ padding: 10px 12px 10px 15px;
8
+ background: none;
9
+ }
10
+ }
11
+ }
12
+
13
+ #cluster-remote-table {
14
+ .cluster-id { width: 70px; }
15
+ .cluster-order { width: 70px; }
16
+ .cluster-color { width: 70px; }
17
+ .odd .colorless { color: #F9F9F9 }
18
+ .even .colorless { color: #FFF }
19
+ tr:hover .colorless { color: #444; opacity: 0.2;}
20
+ }
21
+
22
+ #cluster-remote-form {
23
+ h4.blank-modal-title {
24
+ margin: 0;
25
+ line-height: 28px;
26
+ letter-spacing: 0.05rem;
27
+ font-size: 21px;
28
+ font-weight: 400;
29
+ color: #666;
30
+ }
31
+ }
32
+
33
+
34
+ ul.klastera-option-help {
35
+ padding-left: 20px;
36
+ list-style-type: disc;
37
+ }
38
+
39
+ ul.klastera-option-help li {
40
+ margin-bottom: 10px;
41
+ padding-bottom: 5px;
42
+ border-bottom: 1px solid #DDD;
43
+ }
44
+
45
+ ul.klastera-option-help li b { color: #8A8A8A; font-weight: 400; }
46
+ ul.klastera-option-help li b:first-child { color: #2E5F9B; font-weight: bold; }
@@ -0,0 +1,29 @@
1
+ module Klastera
2
+ class ApplicationController < ::ApplicationController
3
+ protect_from_forgery with: :exception
4
+ layout :disable_in_xhr_requests
5
+ before_action :set_xhr_render_session
6
+ before_action :check_access_to_cluster_admin
7
+
8
+ def check_access_to_cluster_admin
9
+ unless cluster_user.can_admin_clusters?
10
+ redirect_to main_app.root_path, flash: { alert: t('klastera.messages.access_cluster_admin') }
11
+ end
12
+ end
13
+
14
+ if respond_to?(:skip_after_action)
15
+ skip_after_action :verify_authorized
16
+ end
17
+
18
+ private
19
+ def disable_in_xhr_requests
20
+ request.xhr? ? false : nil
21
+ end
22
+
23
+ def set_xhr_render_session
24
+ return unless request.get?
25
+ # request.xhr? return nil when this is not a XHR request, but we need a boolean
26
+ session[:xhr_render] = request.xhr?.present?
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,60 @@
1
+ require_dependency "klastera/application_controller"
2
+
3
+ module Klastera
4
+ class ClustersController < ApplicationController
5
+ before_action :set_cluster, only: %i[ edit update destroy transfer ]
6
+ before_action :set_clusters, only: %i[ index ]
7
+
8
+ def index; end
9
+
10
+ def new
11
+ @cluster = ::Cluster.new
12
+ end
13
+
14
+ def edit; end
15
+
16
+ def transfer
17
+ @transfer = Transfer.new
18
+ end
19
+
20
+ def create
21
+ @cluster = ::Cluster.new(cluster_params)
22
+ if @cluster.save
23
+ @cluster = ::Cluster.new
24
+ set_clusters
25
+ end
26
+ end
27
+
28
+ def update
29
+ if @cluster.update(cluster_params)
30
+ set_clusters
31
+ end
32
+ end
33
+
34
+ def destroy
35
+ new_cluster_id = params.require(:transfer).permit(:new_cluster_id).values.first rescue nil
36
+ @transfer = Transfer.new( current_cluster: @cluster, new_cluster_id: new_cluster_id )
37
+ if @transfer.valid?
38
+ if @transfer.apply!
39
+ @cluster.destroy
40
+ end
41
+ set_clusters
42
+ end
43
+ end
44
+
45
+ private
46
+ def set_cluster
47
+ @cluster = ::Cluster.find(params[:id])
48
+ end
49
+
50
+ def set_clusters
51
+ @clusters = cluster_organization.clusters
52
+ end
53
+
54
+ def cluster_params
55
+ parameters = params.require(:cluster).permit(:nid, :name, :color, :order)
56
+ parameters[:organization_id]=cluster_organization.id
57
+ parameters
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,40 @@
1
+ module Klastera
2
+ module ApplicationHelper
3
+ def ac_errors_as_html_list(errors)
4
+ array_as_html_list(
5
+ errors.messages.inject([]) do |array,(attr,errors_array)|
6
+ array.concat(errors_array)
7
+ array
8
+ end
9
+ )
10
+ end
11
+
12
+ def array_as_html_list(array)
13
+ "<ul style='padding:0;'><li>#{array.join("</li><li>")}</li></ul>".html_safe
14
+ end
15
+
16
+ def method_missing(method, *args, &block)
17
+ if method.to_s.end_with?('_path') or method.to_s.end_with?('_url')
18
+ if main_app.respond_to?(method)
19
+ main_app.send(method, *args)
20
+ else
21
+ super
22
+ end
23
+ else
24
+ super
25
+ end
26
+ end
27
+
28
+ def respond_to?(method, include_priv=false)
29
+ if method.to_s.end_with?('_path') or method.to_s.end_with?('_url')
30
+ if main_app.respond_to?(method,include_priv)
31
+ true
32
+ else
33
+ super
34
+ end
35
+ else
36
+ super
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,5 @@
1
+ module Klastera
2
+ class Cluster < ActiveRecord::Base
3
+ include Klastera::Concerns::Cluster
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ module Klastera
2
+ class ClusterEntity < ActiveRecord::Base
3
+ include Klastera::Concerns::ClusterEntity
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ module Klastera
2
+ class ClusterFilter
3
+ include Klastera::Concerns::ClusterFilter
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ module Klastera
2
+ class ClusterUser < ActiveRecord::Base
3
+ include Klastera::Concerns::ClusterUser
4
+ end
5
+ end
@@ -0,0 +1,67 @@
1
+ module Klastera::Concerns::Cluster
2
+ extend ActiveSupport::Concern
3
+ # 'included do' causes the included code to be evaluated in the
4
+ # context where it is included (cluster.rb), rather than being
5
+ # executed in the module's context (klastera/concerns/models/cluster).
6
+ included do
7
+ self.table_name = 'clusters'
8
+
9
+ attr_reader :last_record
10
+
11
+ REQUIRED_MODE = :required_suborganization.freeze
12
+ OPTIONAL_MODE = :optional_suborganization.freeze
13
+ MODES = [ REQUIRED_MODE, OPTIONAL_MODE ].freeze
14
+
15
+ belongs_to :organization, class_name: Klastera.organization_class
16
+ has_many :cluster_users
17
+ has_many :cluster_entities, dependent: :destroy
18
+
19
+ scope :of, -> (organization,except_ids=[]) {
20
+ _scope = where(organization: organization)
21
+ _scope = _scope.where.not(id: except_ids) unless except_ids.blank?
22
+ _scope
23
+ }
24
+
25
+ validates :name, presence: true
26
+ validates :nid, presence: true
27
+ validates :organization_id, presence: true
28
+
29
+ validates_uniqueness_of :nid, scope: [:nid, :organization_id]
30
+
31
+ before_destroy do |record|
32
+ self.can_transfer_and_destroy?
33
+ end
34
+
35
+ def siblings
36
+ ::Cluster.of(self.organization,[self.id])
37
+ end
38
+
39
+ def is_the_last_record_in_required_suborganization_mode?
40
+ self.organization.required_suborganization_mode? && self.siblings.blank?
41
+ end
42
+
43
+ def required_transfer?
44
+ self.organization.required_suborganization_mode?
45
+ end
46
+
47
+ def has_related_entities_using_it?
48
+ self.cluster_entities.size > 0
49
+ end
50
+
51
+ def can_transfer_and_destroy?
52
+ can_destroy = true
53
+ if is_the_last_record_in_required_suborganization_mode?
54
+ errors.add(:last_record, I18n.t('klastera.messages.cant_delete_the_last_record_in_required_suborganization_mode'))
55
+ can_destroy = false
56
+ end
57
+ can_destroy
58
+ end
59
+
60
+ def display_name_nid
61
+ "#{name} (#{nid==Klastera::UNCLUSTERED_ENTITY ? I18n.t("klastera.#{ Klastera::UNCLUSTERED_ENTITY}") : nid})"
62
+ end
63
+ end
64
+
65
+ module ClassMethods
66
+ end
67
+ end
@@ -0,0 +1,18 @@
1
+ module Klastera::Concerns::ClusterEntity
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ self.table_name = 'cluster_entities'
6
+ belongs_to :entity, polymorphic: true
7
+ belongs_to :cluster, class_name: "::Cluster"
8
+ end
9
+
10
+ module ClassMethods
11
+ def left_join_sources_of(scope_klass)
12
+ scope_klass_arel_table = scope_klass.arel_table
13
+ scope_klass_arel_table.join(arel_table, Arel::Nodes::OuterJoin).on(
14
+ scope_klass_arel_table[:id].eq(arel_table[:entity_id]), arel_table[:entity_type].eq(scope_klass.name)
15
+ ).join_sources
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,22 @@
1
+ module Klastera::Concerns::ClusterFilter
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ include ActiveModel::Model
6
+ include ActiveModel::Validations::Callbacks
7
+
8
+ attr_accessor :cluster_id
9
+ end
10
+
11
+ module ClassMethods
12
+ def attr_accessor(*vars)
13
+ @attributes ||= []
14
+ @attributes.concat vars
15
+ super(*vars)
16
+ end
17
+
18
+ def attributes
19
+ @attributes
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,41 @@
1
+ module Klastera::Concerns::ClusterUser
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ self.table_name = 'cluster_users'
6
+ belongs_to :user
7
+ belongs_to :cluster
8
+ validates :cluster_id, presence: true
9
+ end
10
+
11
+ module ClassMethods
12
+ ##
13
+ # Return a Cluster::ActiveRecord_Relation of organization (and) user
14
+ #
15
+ def clusters_of(organization,and_user=nil)
16
+ and_user_id = and_user.present? ? { users: { id: and_user } } : {}
17
+ ::Cluster.eager_load(cluster_users: :user).where({ organization_id: organization }.merge(and_user_id) ).order(order: :asc)
18
+ end
19
+
20
+ ##
21
+ # Return a User::ActiveRecord_Relation of organization (and) user
22
+ #
23
+ def users_of(organization)
24
+ ::User.eager_load(:cluster_users).where(users: { organization_id: organization } )
25
+ end
26
+
27
+ ##
28
+ # Return a hash of users and its clusters
29
+ #
30
+ def users_hash_of(organization)
31
+ users = {}
32
+ rows = self.users_of(organization).pluck("users.id AS user_id","cluster_users.cluster_id").uniq
33
+ rows.each do |row|
34
+ user_id = row.first
35
+ users[user_id] ||= []
36
+ users[user_id] << row.last
37
+ end
38
+ users
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,115 @@
1
+ module Klastera::Concerns::Clusterizable
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ class MutipleClustersOperationError < StandardError; end
6
+
7
+ belongs_to :cluster
8
+
9
+ has_many :cluster_entities, as: :entity, class_name: "Klastera::ClusterEntity"
10
+ accepts_nested_attributes_for :cluster_entities, reject_if: :all_blank, allow_destroy: true
11
+
12
+ validates :cluster_id, presence: true, if: -> { cluster_id.present? && organization.present? && organization.required_suborganization_mode? }
13
+ validate :at_least_one_cluster_entity, if: -> { organization.present? && organization.required_suborganization_mode? }
14
+ validate :uniqueness_of_cluster_entity_record
15
+
16
+ scope :includes_cluster, -> { includes(cluster_entities: :cluster) }
17
+ scope :with_clusters, -> { includes(cluster_entities: :cluster) }
18
+
19
+ def at_least_one_cluster_entity
20
+ if cluster_entities.length == 0 || cluster_entities.reject{|cluster_entity| cluster_entity._destroy == true}.empty?
21
+ return errors.add(:cluster_entities, I18n.t('klastera.messages.at_least_one_cluster_entity'))
22
+ end
23
+ end
24
+
25
+ def allow_multiple_clusters?(default=:one)
26
+ organization.present? && organization.cluster_cardinality_of(self.class, default: default) == :many
27
+ end
28
+
29
+ ##
30
+ # This method has been optimized to use preloaded associations when available
31
+ # Falls back to efficient single-query approach when associations aren't preloaded
32
+ ##
33
+ def clusters_string_separated_by(separator, attribute=:name)
34
+ # Use Rails cache for frequently accessed cluster strings
35
+ cache_key = [self.cache_key_with_version, :clusters_string, separator, attribute]
36
+
37
+ Rails.cache.fetch(cache_key, expires_in: 30.minutes) do
38
+ Klastera.entity_clusters_string_list!(
39
+ cluster_entities, separator, attribute
40
+ )
41
+ end
42
+ end
43
+
44
+ ##
45
+ # Optimized version without cache for real-time updates
46
+ ##
47
+ def clusters_string_separated_by_uncached(separator, attribute=:name)
48
+ Klastera.entity_clusters_string_list!(
49
+ cluster_entities, separator, attribute
50
+ )
51
+ end
52
+
53
+ def has_one_cluster_entity
54
+ if cluster_entities.length > 1
55
+ return errors.add(:cluster_entities, I18n.t('klastera.messages.has_one_cluster_entity'))
56
+ end
57
+ end
58
+
59
+ def uniqueness_of_cluster_entity_record
60
+ if cluster_entities.map(&:cluster_id).uniq.size != cluster_entities.size
61
+ return errors.add(:cluster_entities, I18n.t('klastera.messages.duplicated_cluster_entity') )
62
+ end
63
+ end
64
+ end
65
+
66
+ module ClassMethods
67
+ def cluster_entity_params
68
+ [ :cluster_id, { cluster_entities_attributes: [:id, :cluster_id, :_destroy] } ]
69
+ end
70
+
71
+ ##
72
+ # Efficiently preload clusters for a collection of entities
73
+ # This prevents N+1 queries when calling clusters_string_separated_by on multiple entities
74
+ ##
75
+ def preload_clusters(collection)
76
+ return collection if collection.empty?
77
+
78
+ ActiveRecord::Associations::Preloader.new.preload(
79
+ collection,
80
+ cluster_entities: :cluster
81
+ )
82
+ collection
83
+ end
84
+
85
+ ##
86
+ # Batch update cluster assignments for multiple entities
87
+ # More efficient than individual updates
88
+ ##
89
+ def batch_assign_clusters(entity_ids, cluster_ids)
90
+ return if entity_ids.empty? || cluster_ids.empty?
91
+
92
+ # Clear existing assignments
93
+ Klastera::ClusterEntity.where(
94
+ entity_type: self.name,
95
+ entity_id: entity_ids
96
+ ).destroy_all
97
+
98
+ # Create new assignments
99
+ cluster_assignments = []
100
+ entity_ids.each do |entity_id|
101
+ cluster_ids.each do |cluster_id|
102
+ cluster_assignments << {
103
+ entity_type: self.name,
104
+ entity_id: entity_id,
105
+ cluster_id: cluster_id,
106
+ created_at: Time.current,
107
+ updated_at: Time.current
108
+ }
109
+ end
110
+ end
111
+
112
+ Klastera::ClusterEntity.insert_all(cluster_assignments) if cluster_assignments.any?
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,49 @@
1
+ module Klastera::Concerns::Organization
2
+ extend ActiveSupport::Concern
3
+ included do
4
+ serialize :cluster_config
5
+ has_many :clusters
6
+
7
+ validates :use_cluster_as, inclusion: { in: ::Cluster::MODES.map{|m|m.to_s}, message: I18n.t('klastera.clusters.wrong_option') }, if: proc{ use_cluster_as.present? }
8
+
9
+ # Return a symbol version of use_cluster_as value
10
+ def cluster_mode
11
+ self.use_cluster_as.to_s.to_sym
12
+ end
13
+
14
+ def cluster_cardinality_of(entity, default: :many)
15
+ if cluster_config.is_a?(Hash)
16
+ entities_cardinality = cluster_config[:entities]&.[](:cardinality)||{}
17
+ entity = entity.to_s.to_sym
18
+ if entities_cardinality.has_key?(entity)
19
+ default = entities_cardinality[entity]
20
+ end
21
+ end
22
+ default.to_s.to_sym
23
+ end
24
+
25
+ ##
26
+ # Return a boolean if one of three of options was set in organization
27
+ # As useless option you can retrieve the value passing false as argument.
28
+ #
29
+ ##
30
+ def is_in_cluster_mode?(return_the_mode=false)
31
+ is_active = cluster_mode.present? && ::Cluster::MODES.include?(cluster_mode)
32
+ ( return_the_mode ? ( is_active ? cluster_mode : false ) : is_active )
33
+ end
34
+
35
+ def required_suborganization_mode?
36
+ cluster_mode == ::Cluster::REQUIRED_MODE
37
+ end
38
+
39
+ def optional_suborganization_mode?
40
+ cluster_mode == ::Cluster::OPTIONAL_MODE
41
+ end
42
+ end
43
+
44
+ module ClassMethods
45
+ def cluster_params
46
+ [ :use_cluster_as ]
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,45 @@
1
+
2
+
3
+ module Klastera::Concerns::Transfer
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ include ActiveModel::Model
8
+ include ActiveModel::Validations::Callbacks
9
+
10
+ attr_accessor :current_cluster, :new_cluster_id
11
+
12
+ validates :current_cluster, presence: true
13
+ validates :new_cluster_id, presence: true, if: proc { self.required_transfer? }
14
+
15
+ validate do
16
+ new_cluster = ::Cluster.find(self.new_cluster_id.to_i) rescue nil
17
+
18
+ if current_cluster.class.name != 'Cluster' || current_cluster.try(:is_the_last_record_in_required_suborganization_mode?)
19
+ errors.add(:current_cluster, I18n.t('klastera.messages.current_cluster.cant_transfer'))
20
+ elsif self.required_transfer? && new_cluster_id.present? && new_cluster.nil?
21
+ errors.add(:new_cluster_id, I18n.t('klastera.messages.new_cluster_id.nil'))
22
+ elsif current_cluster.id == new_cluster.try(:id)
23
+ errors.add(:new_cluster_id, I18n.t('klastera.messages.new_cluster_id.same'))
24
+ elsif new_cluster.present? && current_cluster.organization_id != new_cluster.organization_id
25
+ # Clusters from another organization do not exist
26
+ errors.add(:new_cluster_id, I18n.t('klastera.messages.new_cluster_id.nil'))
27
+ end
28
+ end
29
+
30
+ def required_transfer?
31
+ self.current_cluster.required_transfer? && self.current_cluster.has_related_entities_using_it?
32
+ end
33
+
34
+ ##
35
+ # A returned boolean is expected. It should always be true even nothing is
36
+ # transfered, and it only will return false if creation fails.
37
+ #
38
+ def apply!
39
+ Klastera::ClusterEntity.create(current_cluster.cluster_entities.map{ |relation|
40
+ next if self.new_cluster_id.blank?
41
+ { cluster_id: self.new_cluster_id, entity_id: relation.entity_id, entity_type: relation.entity_type }
42
+ }.compact)
43
+ end
44
+ end
45
+ end