klastera 1.5.3 → 1.5.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) 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 +57 -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/lib/klastera/engine.rb +5 -0
  53. data/lib/klastera/version.rb +3 -0
  54. data/lib/klastera.rb +236 -0
  55. data/lib/tasks/klastera_tasks.rake +32 -0
  56. data/test/controllers/klastera/clusters_controller_test.rb +52 -0
  57. data/test/fixtures/klastera/cluster_users.yml +9 -0
  58. data/test/fixtures/klastera/clusters.yml +11 -0
  59. data/test/integration/navigation_test.rb +8 -0
  60. data/test/klastera_test.rb +7 -0
  61. data/test/models/klastera/cluster_test.rb +9 -0
  62. data/test/models/klastera/cluster_user_test.rb +9 -0
  63. data/test/test_helper.rb +21 -0
  64. metadata +77 -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: b05f393fbe67e0822de3526d5b46dc7081a79226f45b5fe63985424d73331742
4
+ data.tar.gz: '09623883ce4911b3cd0df6382c558c7d82f080eb504f3e0dae4e74262acd8df7'
5
5
  SHA512:
6
- metadata.gz: '096e198a76a0d1f12b83339d76d9fc6f83c64ac3b893b5dafc5ef1b77f4179aee49b146e1be92d2f9ae9b572c4d108dbc3bb9ba80c78a6404459850a1dcd4265'
7
- data.tar.gz: 416ef7b6a278be2686cf613b5ea5382bd728a480486217f2a2b3511677d93b4631a0818cc08a1a94f74bcf7bc57c7598a3f871a901f14f116b817f8b4ec1dcfe
6
+ metadata.gz: 9589942522f4442bf44304cb735a022819bb40f0885c077d77289791caf79f266a1f258d3db7f14fc242d0d80858a8c82027b0314f2faf5ef156e45bcf1c89a7
7
+ data.tar.gz: 3b5d0a9505bd241d3374077ab01d8ddcd2ca83d3f4787763de1658c56f362680b3cb2350e44992a2041834181b7b379c5ad506d4ba83278d140ba6a7d6a775da
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,57 @@
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
+
18
+ def at_least_one_cluster_entity
19
+ if cluster_entities.length == 0 || cluster_entities.reject{|cluster_entity| cluster_entity._destroy == true}.empty?
20
+ return errors.add(:cluster_entities, I18n.t('klastera.messages.at_least_one_cluster_entity'))
21
+ end
22
+ end
23
+
24
+ def allow_multiple_clusters?(default=:one)
25
+ organization.present? && organization.cluster_cardinality_of(self.class, default: default) == :many
26
+ end
27
+
28
+ ##
29
+ # This is a legacy method and we don't recommend using it.
30
+ # Implement directly Klastera.entity_clusters_string_list instead of this method.
31
+ # TODO: In order to deprecate it, you will need to perform some changes in the main app
32
+ ##
33
+ def clusters_string_separated_by(separator,attribute=:name)
34
+ Klastera.entity_clusters_string_list!(
35
+ cluster_entities, separator, attribute
36
+ )
37
+ end
38
+
39
+ def has_one_cluster_entity
40
+ if cluster_entities.length > 1
41
+ return errors.add(:cluster_entities, I18n.t('klastera.messages.has_one_cluster_entity'))
42
+ end
43
+ end
44
+
45
+ def uniqueness_of_cluster_entity_record
46
+ if cluster_entities.map(&:cluster_id).uniq.size != cluster_entities.size
47
+ return errors.add(:cluster_entities, I18n.t('klastera.messages.duplicated_cluster_entity') )
48
+ end
49
+ end
50
+ end
51
+
52
+ module ClassMethods
53
+ def cluster_entity_params
54
+ [ :cluster_id, { cluster_entities_attributes: [:id, :cluster_id, :_destroy] } ]
55
+ end
56
+ end
57
+ 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
@@ -0,0 +1,77 @@
1
+ module Klastera::Concerns::User
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+
6
+ CLUSTER_ROOT = 'root'.freeze
7
+ CLUSTER_ADMIN = 'admin'.freeze
8
+ CLUSTER_USER = 'user'.freeze
9
+
10
+ CLUSTER_ROLES = [ CLUSTER_ROOT, CLUSTER_ADMIN, CLUSTER_USER ].freeze
11
+
12
+ has_many :cluster_users, inverse_of: :user, class_name: ::ClusterUser.to_s
13
+ accepts_nested_attributes_for :cluster_users, reject_if: :all_blank, allow_destroy: true
14
+ validates :cluster_role, presence: true, if: -> { organization.present? && organization.is_in_cluster_mode? }
15
+ validates :cluster_role, inclusion: { in: CLUSTER_ROLES , message: I18n.t('klastera.clusters.wrong_option') }, if: -> { cluster_role.present? }
16
+ validate :at_least_one_role, if: -> { use_cluster? && try(:need_cluster_assignation?) }
17
+
18
+ def use_cluster?; cluster_role.present?; end
19
+
20
+ def is_a_cluster_root?; cluster_role == CLUSTER_ROOT; end
21
+ def is_a_cluster_admin?; cluster_role == CLUSTER_ADMIN; end
22
+ def is_a_cluster_user?; cluster_role == CLUSTER_USER; end
23
+
24
+ def need_cluster_assignation?
25
+ organization.present? && organization.required_suborganization_mode? && is_a_cluster_user?
26
+ end
27
+
28
+ def can_admin_clusters?
29
+ organization.present? && organization.is_in_cluster_mode? && ( is_a_cluster_admin? || is_a_cluster_root? )
30
+ end
31
+
32
+ #
33
+ # It tells if this user should see what they cluster role or organization dictates
34
+ # Adminers and users from show cluster modes skip cluster clause
35
+ def cannot_skip_cluster_clause?
36
+ ! ( is_a_cluster_admin? || is_a_cluster_root? || ( organization.present? && organization.optional_suborganization_mode? && cluster_users.blank? ) )
37
+ end
38
+
39
+ #
40
+ # We will try to create a cluster_user record whether
41
+ # the organization is using force mode (if cluster_id isn't present, it will raise a validation error)
42
+ # or
43
+ # cluster_id is present (on show/use mode organizations).
44
+ #
45
+ def try_to_clusterify!(role,cluster_ids)
46
+ if organization.present? && organization.is_in_cluster_mode?
47
+ self.cluster_role = role
48
+ if ( organization.present? && organization.required_suborganization_mode? ) || cluster_ids.present?
49
+ timestamp = DateTime.now.to_i
50
+ nested_attributes = {}
51
+ cluster_ids.each do |cluster_id|
52
+ nested_attributes["#{cluster_id}_#{timestamp}"] = {
53
+ cluster_id: cluster_id,
54
+ _destroy: false
55
+ }
56
+ end
57
+ self.cluster_users_attributes = nested_attributes
58
+ end
59
+ end
60
+ end
61
+
62
+ private
63
+
64
+ def at_least_one_role
65
+ if cluster_users.none?
66
+ errors.add(:base, I18n.t('klastera.user.you_need_add_at_least_one_cluster'))
67
+ end
68
+ end
69
+
70
+ end
71
+
72
+ module ClassMethods
73
+ def cluster_params
74
+ [ :cluster_role, { cluster_users_attributes: [:id, :cluster_id, :_destroy] } ]
75
+ end
76
+ end
77
+ end