klastera 1.2.3.2 → 1.3.1
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.
- checksums.yaml +4 -4
- data/app/assets/stylesheets/klastera/clusters.css.scss +36 -1
- data/app/controllers/klastera/clusters_controller.rb +4 -6
- data/app/models/klastera/concerns/cluster.rb +3 -20
- data/app/models/klastera/concerns/cluster_filter.rb +1 -4
- data/app/models/klastera/concerns/cluster_user.rb +20 -36
- data/app/models/klastera/concerns/clusterizable.rb +15 -5
- data/app/models/klastera/concerns/organization.rb +1 -1
- data/app/models/klastera/concerns/transfer.rb +13 -22
- data/app/views/klastera/clusters/_form.html.erb +4 -4
- data/app/views/klastera/clusters/_form_transfer.html.erb +2 -2
- data/app/views/klastera/clusters/_table.html.erb +4 -12
- data/app/views/klastera/clusters/index.html.erb +1 -1
- data/app/views/layouts/klastera/_cluster_entity_fields.html.erb +1 -1
- data/app/views/layouts/klastera/_cluster_filter.html.erb +1 -1
- data/app/views/layouts/klastera/_cluster_selector.html.erb +1 -1
- data/app/views/layouts/klastera/_nested_cluster_entity.html.erb +2 -0
- data/app/views/layouts/klastera/_options.html.erb +1 -17
- data/config/locales/es.yml +1 -0
- data/lib/klastera.rb +182 -137
- data/lib/klastera/version.rb +2 -2
- data/lib/tasks/klastera_tasks.rake +5 -0
- metadata +2 -3
- data/app/helpers/klastera/clusters_helper.rb +0 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a8ffbcdd772da4880e6fac3a2fa9165754a873673afb5f2108161d099e634622
|
4
|
+
data.tar.gz: 8541bb3c9c26b8e0084e42b91ef9262b443dbaf1250e4bd209a2fde9db6d92a9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e43f85ef19e054e32274ad4c5c99db112333c76fa831383e72f3e0f7d7ad312d5b48519c33794a46ad3148dd4d50cca62117620e756b12933f0889a9891ad631
|
7
|
+
data.tar.gz: dc98ab10fc1588bec841af4b1e3a4caee5b976986e8023b7528c70190ee2b27fb2e230855773aaa1fcf1a28392380b9f63d186b9165e4f5704acf2631cd3d40f
|
@@ -8,4 +8,39 @@
|
|
8
8
|
background: none;
|
9
9
|
}
|
10
10
|
}
|
11
|
-
}
|
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; }
|
@@ -33,13 +33,11 @@ module Klastera
|
|
33
33
|
|
34
34
|
def destroy
|
35
35
|
new_cluster_id = params.require(:transfer).permit(:new_cluster_id).values.first rescue nil
|
36
|
-
@transfer = Transfer.new(
|
37
|
-
current_cluster: @cluster,
|
38
|
-
new_cluster_id: new_cluster_id
|
39
|
-
)
|
36
|
+
@transfer = Transfer.new( current_cluster: @cluster, new_cluster_id: new_cluster_id )
|
40
37
|
if @transfer.valid?
|
41
|
-
@transfer.
|
42
|
-
|
38
|
+
if @transfer.apply!
|
39
|
+
@cluster.destroy
|
40
|
+
end
|
43
41
|
set_clusters
|
44
42
|
end
|
45
43
|
end
|
@@ -11,6 +11,8 @@ module Klastera::Concerns::Cluster
|
|
11
11
|
MODES = [ :required_suborganization, :optional_suborganization, :optional_filter ].freeze
|
12
12
|
|
13
13
|
belongs_to :organization, class_name: Klastera.organization_class
|
14
|
+
has_many :cluster_users
|
15
|
+
has_many :cluster_entities, dependent: :destroy
|
14
16
|
|
15
17
|
scope :of, -> (organization,except_ids=[]) {
|
16
18
|
_scope = where(organization: organization)
|
@@ -41,7 +43,7 @@ module Klastera::Concerns::Cluster
|
|
41
43
|
end
|
42
44
|
|
43
45
|
def has_related_entities_using_it?
|
44
|
-
|
46
|
+
self.cluster_entities.size > 0
|
45
47
|
end
|
46
48
|
|
47
49
|
def can_transfer_and_destroy?
|
@@ -59,24 +61,5 @@ module Klastera::Concerns::Cluster
|
|
59
61
|
end
|
60
62
|
|
61
63
|
module ClassMethods
|
62
|
-
def related_entities(attr_needed: :class_name, macro: :has_many)
|
63
|
-
::Cluster.reflections.map do |association_name, reflection|
|
64
|
-
reflection.send(attr_needed) if reflection.macro == macro
|
65
|
-
end.compact
|
66
|
-
end
|
67
|
-
|
68
|
-
def total_records_assign_to(cluster_instance)
|
69
|
-
related_entities(attr_needed: :name).inject(0) do |total, association_name|
|
70
|
-
entity = cluster_instance.send(association_name)
|
71
|
-
if entity.respond_to?(:cluster_id)
|
72
|
-
total+=entity.where(cluster_id: cluster_instance.id).count
|
73
|
-
end
|
74
|
-
total
|
75
|
-
end
|
76
|
-
end
|
77
|
-
|
78
|
-
def modes_as_strings
|
79
|
-
::Cluster::MODES.map{|m|m.to_s}
|
80
|
-
end
|
81
64
|
end
|
82
65
|
end
|
@@ -4,11 +4,8 @@ module Klastera::Concerns::ClusterFilter
|
|
4
4
|
included do
|
5
5
|
include ActiveModel::Model
|
6
6
|
include ActiveModel::Validations::Callbacks
|
7
|
-
attr_accessor :cluster_id
|
8
7
|
|
9
|
-
|
10
|
-
self.cluster_id == 'without_cluster' ? nil : self.cluster_id
|
11
|
-
end
|
8
|
+
attr_accessor :cluster_id
|
12
9
|
end
|
13
10
|
|
14
11
|
module ClassMethods
|
@@ -3,55 +3,39 @@ module Klastera::Concerns::ClusterUser
|
|
3
3
|
|
4
4
|
included do
|
5
5
|
self.table_name = 'cluster_users'
|
6
|
-
|
7
6
|
belongs_to :user
|
8
7
|
belongs_to :cluster
|
9
|
-
|
10
8
|
validates :cluster_id, presence: true
|
11
|
-
|
12
|
-
scope :of, -> (user,organization) {
|
13
|
-
Rails.logger.warn("DON'T USE THIS SCOPE DIRECTLY: Use ::ClusterUser.clusters_from class method instead!")
|
14
|
-
includes(:cluster).where(user_id: user, "clusters.organization_id": organization)
|
15
|
-
}
|
16
9
|
end
|
17
10
|
|
18
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) )
|
18
|
+
end
|
19
19
|
|
20
20
|
##
|
21
|
-
|
22
|
-
|
23
|
-
def
|
24
|
-
|
25
|
-
organization_cluster_ids = ::Cluster.of(organization).map(&:id)
|
26
|
-
::ClusterUser.includes(:user).where(cluster_id: organization_cluster_ids).each do |cluster_user|
|
27
|
-
user_id = cluster_user.user_id || :uncluster
|
28
|
-
users_hash[user_id] ||= []
|
29
|
-
users_hash[user_id] << cluster_user.cluster_id
|
30
|
-
end
|
31
|
-
users_hash
|
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 } )
|
32
25
|
end
|
33
26
|
|
34
27
|
##
|
35
|
-
#
|
28
|
+
# Return a hash of users and its clusters
|
36
29
|
#
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
# TODO: resolve it without quering twice
|
45
|
-
clusters_id = ::ClusterUser.of(user,organization).map(&:cluster_id)
|
46
|
-
clusters = ::Cluster.where(id: clusters_id)
|
47
|
-
# Add a empty cluster instance to handle models without a cluster assignation. Only for use and show modes
|
48
|
-
if organization.optional_mode?
|
49
|
-
clusters << ::Cluster.new({nid: :without_cluster, name: I18n.t('klastera.without_cluster')})
|
50
|
-
end
|
51
|
-
else
|
52
|
-
clusters = ::Cluster.of(organization)
|
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
|
53
37
|
end
|
54
|
-
|
38
|
+
users
|
55
39
|
end
|
56
40
|
end
|
57
41
|
end
|
@@ -9,10 +9,7 @@ module Klastera::Concerns::Clusterizable
|
|
9
9
|
|
10
10
|
validates :cluster_id, presence: true, if: proc { self.try(:cluster_id) && self.organization.required_suborganization_mode? }
|
11
11
|
validate :at_least_one_cluster_entity, if: proc { self.organization.required_suborganization_mode? }
|
12
|
-
|
13
|
-
scope :related_clusters, ->() {
|
14
|
-
includes(:cluster_entities).where("cluster_entities.entity_id = #{self.table_name}.id")
|
15
|
-
}
|
12
|
+
validate :uniqueness_of_cluster_entity_record
|
16
13
|
|
17
14
|
def at_least_one_cluster_entity
|
18
15
|
if cluster_entities.length == 0 || cluster_entities.reject{|cluster_entity| cluster_entity._destroy == true}.empty?
|
@@ -20,8 +17,15 @@ module Klastera::Concerns::Clusterizable
|
|
20
17
|
end
|
21
18
|
end
|
22
19
|
|
20
|
+
##
|
21
|
+
# This is a legacy method and we don't recommend using it.
|
22
|
+
# Implement directly Klastera.entity_clusters_string_list instead of this method.
|
23
|
+
# TODO: In order to deprecate it, you will need to perform some changes in the main app
|
24
|
+
##
|
23
25
|
def clusters_string_separated_by(separator,attribute=:name)
|
24
|
-
|
26
|
+
Klastera.entity_clusters_string_list!(
|
27
|
+
self.cluster_entities, separator, attribute
|
28
|
+
)
|
25
29
|
end
|
26
30
|
|
27
31
|
def has_one_cluster_entity
|
@@ -29,6 +33,12 @@ module Klastera::Concerns::Clusterizable
|
|
29
33
|
return errors.add(:cluster_entities, I18n.t('klastera.messages.has_one_cluster_entity'))
|
30
34
|
end
|
31
35
|
end
|
36
|
+
|
37
|
+
def uniqueness_of_cluster_entity_record
|
38
|
+
if cluster_entities.map(&:cluster_id).uniq.size != cluster_entities.size
|
39
|
+
return errors.add(:cluster_entities, I18n.t('klastera.messages.duplicated_cluster_entity') )
|
40
|
+
end
|
41
|
+
end
|
32
42
|
end
|
33
43
|
|
34
44
|
module ClassMethods
|
@@ -4,7 +4,7 @@ module Klastera::Concerns::Organization
|
|
4
4
|
|
5
5
|
has_many :clusters
|
6
6
|
|
7
|
-
validates :use_cluster_as, inclusion: { in: ::Cluster.
|
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
8
|
|
9
9
|
# Return a symbol version of use_cluster_as value
|
10
10
|
def cluster_mode
|
@@ -7,20 +7,14 @@ module Klastera::Concerns::Transfer
|
|
7
7
|
include ActiveModel::Model
|
8
8
|
include ActiveModel::Validations::Callbacks
|
9
9
|
|
10
|
-
attr_accessor :current_cluster, :new_cluster_id
|
10
|
+
attr_accessor :current_cluster, :new_cluster_id
|
11
11
|
|
12
12
|
validates :current_cluster, presence: true
|
13
13
|
validates :new_cluster_id, presence: true, if: proc { self.required_transfer? }
|
14
14
|
|
15
15
|
validate do
|
16
16
|
new_cluster = ::Cluster.find(self.new_cluster_id.to_i) rescue nil
|
17
|
-
|
18
|
-
# In my time, to_a? didnt work
|
19
|
-
# current_cluster.class returned:
|
20
|
-
# Cluster(id: integer, name: string, nid: text, organization_id: integer, created_at: datetime, updated_at: datetime, color: string)
|
21
|
-
#
|
22
|
-
# If you see this, please fixe it. Thanks
|
23
|
-
#
|
17
|
+
|
24
18
|
if current_cluster.class.name != 'Cluster' || current_cluster.try(:is_the_last_record_in_required_suborganization_mode?)
|
25
19
|
errors.add(:current_cluster, I18n.t('klastera.messages.current_cluster.cant_transfer'))
|
26
20
|
elsif self.required_transfer? && new_cluster_id.present? && new_cluster.nil?
|
@@ -33,22 +27,19 @@ module Klastera::Concerns::Transfer
|
|
33
27
|
end
|
34
28
|
end
|
35
29
|
|
36
|
-
##
|
37
|
-
#
|
38
|
-
#
|
39
|
-
def to!(related_entities)
|
40
|
-
self.entities_transfered = 0
|
41
|
-
related_entities.each do |entity|
|
42
|
-
if entity.is_a?(Class) && entity.respond_to?(:cluster_id)
|
43
|
-
self.entities_transfered += entity.where(
|
44
|
-
cluster_id: self.current_cluster.id
|
45
|
-
).update_all(cluster_id: self.new_cluster_id)
|
46
|
-
end
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
50
30
|
def required_transfer?
|
51
31
|
self.current_cluster.required_transfer? && self.current_cluster.has_related_entities_using_it?
|
52
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
|
53
44
|
end
|
54
45
|
end
|
@@ -3,16 +3,16 @@
|
|
3
3
|
|
4
4
|
<div class="<%=classes_for_remote_modal_body()%>">
|
5
5
|
<div class="row">
|
6
|
-
<div class="col-xs-
|
6
|
+
<div class="col-xs-6">
|
7
7
|
<%= f.input :name, as: :string, label: t('klastera.cluster_name') %>
|
8
8
|
</div>
|
9
|
-
<div class="col-xs-
|
9
|
+
<div class="col-xs-6">
|
10
10
|
<%= f.input :nid, as: :string, label: t('klastera.cluster_nid') %>
|
11
11
|
</div>
|
12
|
-
<div class="col-xs-
|
12
|
+
<div class="col-xs-6">
|
13
13
|
<%= f.input :color, as: :string, label: t('klastera.cluster_color'), input_html: { class: 'color-picker'} %>
|
14
14
|
</div>
|
15
|
-
<div class="col-xs-
|
15
|
+
<div class="col-xs-6">
|
16
16
|
<%= f.input :order, as: :string, label: t('klastera.cluster_order') %>
|
17
17
|
</div>
|
18
18
|
</div>
|
@@ -2,8 +2,8 @@
|
|
2
2
|
<%= simple_form_for( @transfer, url: cluster_path(@cluster), method: :delete, remote: true, html: { autocomplete: :off, class: 'destroy-form slim-form-field' } ) do |f| %>
|
3
3
|
<%=render 'layouts/remote_form/header', o: @cluster, title: title, fa: :qrcode %>
|
4
4
|
<div class="<%=classes_for_remote_modal_body()%>">
|
5
|
-
<h4 class="text-center">Esta acción es irreversible
|
6
|
-
<% if
|
5
|
+
<h4 class="blank-modal-title text-center">Esta acción es irreversible<br/>¿Está seguro?</h4>
|
6
|
+
<% if @cluster.cluster_entities.size > 0 %>
|
7
7
|
<br />
|
8
8
|
<div class="row">
|
9
9
|
<% if can_transfer_and_destroy %>
|
@@ -1,4 +1,4 @@
|
|
1
|
-
<table class="table table-striped table-hover table-condensed table-bordered datatable">
|
1
|
+
<table class="table table-striped table-hover table-condensed table-bordered datatable table">
|
2
2
|
<thead>
|
3
3
|
<tr>
|
4
4
|
<th class="text-center cluster-id"><%=t('klastera.cluster_id')%></th>
|
@@ -6,7 +6,7 @@
|
|
6
6
|
<th class="text-center cluster-color"><%=t('klastera.cluster_color')%></th>
|
7
7
|
<th class="text-center cluster-name"><%=t('klastera.cluster_name')%></th>
|
8
8
|
<th class="text-center cluster-nid"><%=t('klastera.cluster_nid')%></th>
|
9
|
-
<th class="cogs-actions
|
9
|
+
<th class="cogs-actions two-icons"><span class="fa fa-cogs"></span></th>
|
10
10
|
</tr>
|
11
11
|
</thead>
|
12
12
|
<tbody>
|
@@ -19,7 +19,7 @@
|
|
19
19
|
</td>
|
20
20
|
<td class="text-center cluster-name"><%= cluster.name %></td>
|
21
21
|
<td class="text-center cluster-nid"><%= cluster.nid %></td>
|
22
|
-
<td class="cogs-actions
|
22
|
+
<td class="cogs-actions two-icons">
|
23
23
|
<%= link_to edit_cluster_path(cluster), class:'btn btn-primary btn-xs', title: t('shared.actions.edit'), "data-toggle": "modal", "data-target": "#remote-modal-block" do %>
|
24
24
|
<span class="fa fa-pencil-square-o"></span>
|
25
25
|
<% end %>
|
@@ -30,12 +30,4 @@
|
|
30
30
|
</tr>
|
31
31
|
<% end %>
|
32
32
|
</tbody>
|
33
|
-
</table>
|
34
|
-
<style type="text/css">
|
35
|
-
.cluster-id { width: 70px; }
|
36
|
-
.cluster-order { width: 70px; }
|
37
|
-
.cluster-color { width: 70px; }
|
38
|
-
.odd .colorless { color: #F9F9F9 }
|
39
|
-
.even .colorless { color: #FFF }
|
40
|
-
tr:hover .colorless { color: #F5F5F5 }
|
41
|
-
</style>
|
33
|
+
</table>
|
@@ -1,6 +1,6 @@
|
|
1
1
|
<tr class="nested-fields">
|
2
2
|
<td class="field">
|
3
|
-
<%= f.select :cluster_id,
|
3
|
+
<%= f.select :cluster_id, @clusters_session.map{|c|[c.name,c.id]}, { include_blank: true }, { class: 'form-control' } %>
|
4
4
|
</td>
|
5
5
|
<td class="action vertical-align-middle text-center" width="44">
|
6
6
|
<%= link_to_remove_association f, class: 'btn btn-danger btn-xs text-danger' do %>
|
@@ -7,7 +7,7 @@
|
|
7
7
|
<%=yield(f) if block_given? %>
|
8
8
|
|
9
9
|
<% if cluster_organization.is_in_cluster_mode? %>
|
10
|
-
<% cluster_collection =
|
10
|
+
<% cluster_collection = cluster_of_my_own.map{|c|[c.name,(c.id||c.nid)]} %>
|
11
11
|
<% if cluster_collection.size > 1 %>
|
12
12
|
<div class="inline-label-control-block">
|
13
13
|
<%= f.input :cluster_id, collection: cluster_collection, prompt: t('klastera.clusters.all'), label: false, wrapper: false %>
|
@@ -4,7 +4,7 @@
|
|
4
4
|
%>
|
5
5
|
<% if cluster_organization.is_in_cluster_mode? || other_visibility_reason %>
|
6
6
|
<%
|
7
|
-
cluster_collection = @
|
7
|
+
cluster_collection = @clusters_session || cluster_of_my_own
|
8
8
|
label = t('klastera.cluster.title')
|
9
9
|
%>
|
10
10
|
<% if f.nil? %>
|
@@ -1,4 +1,5 @@
|
|
1
1
|
<% if cluster_organization.is_in_cluster_mode? %>
|
2
|
+
<% @clusters_session = cluster_of_my_own %>
|
2
3
|
<div class="col-xs-12">
|
3
4
|
<% if hide_title||=false %>
|
4
5
|
<div class="form-group file required <%=f.object.class.name.parameterize%>_cluster_entity<%=' has-error' if f.object.errors.has_key?(:cluster_entities)%>">
|
@@ -26,6 +27,7 @@
|
|
26
27
|
<table id="cluster-entities" class="table table-striped">
|
27
28
|
<tbody class="cluster-entity-rows">
|
28
29
|
<%= f.fields_for :cluster_entities do |cluster_entity|%>
|
30
|
+
<% next unless @clusters_session.map(&:id).include?(cluster_entity.try(:object).try(:cluster_id)) %>
|
29
31
|
<%= render 'layouts/klastera/cluster_entity_fields', f: cluster_entity %>
|
30
32
|
<% end %>
|
31
33
|
</body>
|
@@ -19,20 +19,4 @@
|
|
19
19
|
<script type="text/javascript">
|
20
20
|
var popoverTemplate = '<div class="popover" role="tooltip"><div class="arrow"></div><h3 class="popover-title"></h3><div class="popover-content"></div></div>'
|
21
21
|
$('.klastera-cluster-option').popover({ template: popoverTemplate, html: true, trigger: 'click', });
|
22
|
-
</script>
|
23
|
-
|
24
|
-
<style type="text/css">
|
25
|
-
ul.klastera-option-help {
|
26
|
-
padding-left: 20px;
|
27
|
-
list-style-type: disc;
|
28
|
-
}
|
29
|
-
|
30
|
-
ul.klastera-option-help li {
|
31
|
-
margin-bottom: 10px;
|
32
|
-
padding-bottom: 5px;
|
33
|
-
border-bottom: 1px solid #DDD;
|
34
|
-
}
|
35
|
-
|
36
|
-
ul.klastera-option-help li b { color: #8A8A8A; font-weight: 400; }
|
37
|
-
ul.klastera-option-help li b:first-child { color: #2E5F9B; font-weight: bold; }
|
38
|
-
</style>
|
22
|
+
</script>
|
data/config/locales/es.yml
CHANGED
@@ -61,6 +61,7 @@ es:
|
|
61
61
|
at_least_one_cluster_entity: Debe agregar al menos un cluster
|
62
62
|
record_action_successfully: Registro %{a} exitosamente
|
63
63
|
cant_delete_the_last_record_in_required_suborganization_mode: No se puede eliminar el único cluster de esta organización
|
64
|
+
duplicated_cluster_entity: Hay un cluster duplicado
|
64
65
|
new_cluster_id:
|
65
66
|
nil: Cluster no existe
|
66
67
|
same: Cluster no puede ser el mismo
|
data/lib/klastera.rb
CHANGED
@@ -5,8 +5,17 @@ module Klastera
|
|
5
5
|
|
6
6
|
extend ActiveSupport::Concern
|
7
7
|
|
8
|
+
UNCLUSTERED_ENTITY = 'without_cluster'
|
9
|
+
KLSTR_HELPERS = %i[
|
10
|
+
cluster_user cluster_organization user_has_more_than_one_cluster cluster_scope cluster_of_my_own
|
11
|
+
user_clusters_string_list set_collection_before_group_by_entity __cluster_scope
|
12
|
+
]
|
13
|
+
|
8
14
|
class << self
|
9
15
|
|
16
|
+
##
|
17
|
+
#
|
18
|
+
#
|
10
19
|
def set_cluster_entities_attributes!(entity,array_cluster_ids)
|
11
20
|
cluster_entities_attributes = {}
|
12
21
|
entity_cluster_entities = entity.try(:cluster_entities) || []
|
@@ -29,94 +38,119 @@ module Klastera
|
|
29
38
|
cluster_entities_attributes
|
30
39
|
end
|
31
40
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
def
|
36
|
-
self.filter_clusterized_collection_with!(
|
37
|
-
organization,
|
38
|
-
cluster_id,
|
39
|
-
self.cluster_scope!(user,organization,scope,includes)
|
40
|
-
)
|
41
|
-
end
|
42
|
-
|
43
|
-
#
|
44
|
-
# In order to this works, active_record_collection argument
|
45
|
-
# should be passed through cluster_scope! method before.
|
46
|
-
#
|
47
|
-
def filter_clusterized_collection_with!(cluster_organization,cluster_id,active_record_collection)
|
48
|
-
if cluster_organization.is_in_cluster_mode?
|
49
|
-
if cluster_id.present?
|
50
|
-
cluster_array = [cluster_id]
|
51
|
-
# Based on force/use/show definition we don't really need this validation.
|
52
|
-
# A force cluster organization won't return an entity out of a cluster, but
|
53
|
-
# we don't know if the clusterized entities are fully definition-compliant.
|
54
|
-
cluster_array << nil if cluster_organization.optional_suborganization_mode?
|
55
|
-
active_record_collection = active_record_collection.joins(:cluster_entities).where("cluster_entities.cluster_id": cluster_array)
|
56
|
-
end
|
57
|
-
# you may use a block only with clusterizable data
|
58
|
-
yield(active_record_collection) if block_given?
|
59
|
-
end
|
60
|
-
active_record_collection
|
61
|
-
end
|
62
|
-
|
63
|
-
#
|
64
|
-
#
|
65
|
-
#
|
66
|
-
def set_collection_before_group_by_entity!(cluster_organization,active_record_collection,model_class,entity_params)
|
41
|
+
##
|
42
|
+
#
|
43
|
+
#
|
44
|
+
def set_collection_before_group_by_entity!(active_record_collection,entity_params,organization)
|
67
45
|
entity_params_keys = [:entity_name,:entity_attribute,:entity_id,:entity_id_attribute,:unamed]
|
46
|
+
entity_params[:entity_id] ||= nil #Ensures the entity_id attribute presence even if there is no filter
|
68
47
|
entity_params = entity_params.slice(*entity_params_keys).values
|
69
|
-
|
48
|
+
model_class = active_record_collection.model.base_class
|
49
|
+
model_relations = model_class.reflections.keys
|
50
|
+
if model_relations.include?(entity_params[0])
|
70
51
|
entity_params << "#{entity_params[0]}_id".to_sym
|
71
52
|
if entity_params[0] == 'cluster'
|
72
|
-
entity_params <<
|
73
|
-
active_record_collection = Klastera.
|
74
|
-
|
53
|
+
entity_params << I18n.t("klastera.#{UNCLUSTERED_ENTITY}") if organization.is_in_cluster_mode?
|
54
|
+
active_record_collection = Klastera.filter_clusterized!(
|
55
|
+
active_record_collection,
|
75
56
|
entity_params[2],
|
76
|
-
|
57
|
+
organization
|
77
58
|
)
|
78
59
|
end
|
79
|
-
yield(
|
80
|
-
active_record_collection,
|
81
|
-
entity_params_keys.zip(entity_params).to_h)
|
60
|
+
yield( active_record_collection, entity_params_keys.zip(entity_params).to_h )
|
82
61
|
end
|
83
62
|
end
|
84
63
|
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
def
|
89
|
-
|
64
|
+
#
|
65
|
+
# In order to this work, the active_record_collection argument should be a cluster_scope! return.
|
66
|
+
#
|
67
|
+
def filter_clusterized!(active_record_collection,cluster_id,organization)
|
68
|
+
if organization.is_in_cluster_mode? && cluster_id.present?
|
69
|
+
# Ensures that an array of ids is used in the statement
|
70
|
+
cluster_array = cluster_id.is_a?(Array) ? cluster_id : [cluster_id]
|
71
|
+
# If we receive a UNCLUSTERED_ENTITY request, it means that we should filter by entities without clusters.
|
72
|
+
# The optional_mode?(show/use) condition add to this method an ambivalent use,
|
73
|
+
# where you can filter a unique value or multiple including NULL ones.
|
74
|
+
cluster_array << nil if cluster_array.delete(UNCLUSTERED_ENTITY) || organization.optional_mode?
|
75
|
+
active_record_collection = active_record_collection.where(cluster_entities: { cluster_id: cluster_array.uniq })
|
76
|
+
# You should use a block with clusterable data only
|
77
|
+
yield(active_record_collection) if block_given?
|
78
|
+
end
|
79
|
+
active_record_collection
|
90
80
|
end
|
91
81
|
|
92
82
|
##
|
93
|
-
#
|
94
|
-
# organization if the cluster mode is not active.
|
83
|
+
# If the cluster mode is not active, this returns a scope filtered by clusters of its organization/users/cluster
|
95
84
|
#
|
96
|
-
def cluster_scope!(user,organization,
|
85
|
+
def cluster_scope!(scope,user,organization,cluster_id=nil)
|
97
86
|
scope_klass = scope_class(scope).where(organization_id: organization)
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
cluster_ids =
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
or_these_cluster_ids = cluster_ids.present? ? " OR cluster_entities.cluster_id IN (#{cluster_ids.join(",")})" : ""
|
107
|
-
scope_klass = scope_klass.joins("
|
108
|
-
LEFT OUTER JOIN cluster_entities
|
109
|
-
ON entity_id = #{scope.table_name}.id
|
110
|
-
AND entity_type = '#{scope}'
|
111
|
-
").where("cluster_entities.id IS NULL#{or_these_cluster_ids}")
|
112
|
-
end
|
113
|
-
# Provisional fix to avoid SQL clashes due to DISTINCT ON clause
|
114
|
-
scope_klass = scope_class(scope).where(organization_id: organization).where(id: scope_klass.map(&:id))
|
87
|
+
if organization.is_in_cluster_mode?
|
88
|
+
|
89
|
+
if cluster_id.present?
|
90
|
+
# Ensures that an array of id is used in the statement
|
91
|
+
cluster_ids = cluster_id.is_a?(Array) ? cluster_id : [cluster_id]
|
92
|
+
else
|
93
|
+
clusters = cluster_of!(user,organization)
|
94
|
+
cluster_ids = clusters.map(&:id).compact
|
115
95
|
end
|
96
|
+
|
97
|
+
scope_klass = scope_klass.select("DISTINCT ON (#{scope.table_name}.id) #{scope.table_name}.id, #{scope.table_name}.*")
|
98
|
+
|
99
|
+
if organization.required_suborganization_mode?
|
100
|
+
scope_klass = scope_klass.joins(:cluster_entities).where( cluster_entities: { cluster_id: cluster_ids } )
|
101
|
+
else
|
102
|
+
or_these_cluster_ids = cluster_ids.present? ? " OR cluster_entities.cluster_id IN (#{cluster_ids.join(",")})" : ""
|
103
|
+
scope_klass = scope_klass.joins("
|
104
|
+
LEFT OUTER JOIN cluster_entities
|
105
|
+
ON entity_id = #{scope.table_name}.id
|
106
|
+
AND entity_type = '#{scope}'
|
107
|
+
").where("cluster_entities.id IS NULL#{or_these_cluster_ids}")
|
108
|
+
end
|
109
|
+
|
110
|
+
# Provisional fix to avoid unresolved SQL clashes with the main application due to DISTINCT ON clause
|
111
|
+
scope_klass = scope_class(scope).eager_load(:cluster_entities).where(id: scope_klass.map(&:id), organization_id: organization)
|
116
112
|
end
|
117
113
|
scope_klass
|
118
114
|
end
|
119
115
|
|
116
|
+
#
|
117
|
+
# Untested version for more optimized queries
|
118
|
+
#
|
119
|
+
def __cluster_scope!(scope,user,organization,cluster_id=nil,same_scope:false)
|
120
|
+
scope_klass = scope_class(scope)
|
121
|
+
if organization.is_in_cluster_mode?
|
122
|
+
scope_klass = scope_klass.includes(:organization)
|
123
|
+
|
124
|
+
if cluster_id.present?
|
125
|
+
cluster_ids = cluster_id.is_a?(Array) ? cluster_id : [cluster_id]
|
126
|
+
else
|
127
|
+
clusters = cluster_of!(user,organization)
|
128
|
+
cluster_ids = clusters.map(&:id).compact
|
129
|
+
end
|
130
|
+
|
131
|
+
scope_klass = scope_klass.select("DISTINCT ON (#{scope.table_name}.id) #{scope.table_name}.id, #{scope.table_name}.*, clusters.*")
|
132
|
+
|
133
|
+
if organization.required_suborganization_mode?
|
134
|
+
scope_klass = scope_klass.includes(cluster_entities: :cluster).where( cluster_entities: { cluster_id: cluster_ids } )
|
135
|
+
else
|
136
|
+
or_these_cluster_ids = cluster_ids.present? ? " OR cluster_entities.cluster_id IN (#{cluster_ids.join(",")})" : ""
|
137
|
+
scope_klass = scope_klass.joins("
|
138
|
+
LEFT OUTER JOIN cluster_entities
|
139
|
+
ON entity_id = #{scope.table_name}.id
|
140
|
+
AND entity_type = '#{scope}'
|
141
|
+
").joins("
|
142
|
+
LEFT OUTER JOIN clusters
|
143
|
+
ON clusters.id = cluster_entities.cluster_id
|
144
|
+
").where("cluster_entities.id IS NULL#{or_these_cluster_ids}")
|
145
|
+
end
|
146
|
+
# Provisional fix to avoid unresolved SQL clashes with the main application due to DISTINCT ON clause
|
147
|
+
unless same_scope
|
148
|
+
scope_klass = scope_class(scope).eager_load(:cluster_entities).where(id: scope_klass.map(&:id))
|
149
|
+
end
|
150
|
+
end
|
151
|
+
scope_klass.where(organization_id: organization)
|
152
|
+
end
|
153
|
+
|
120
154
|
##
|
121
155
|
# TODO:
|
122
156
|
# Implement a validation to ensure that
|
@@ -129,67 +163,44 @@ module Klastera
|
|
129
163
|
end
|
130
164
|
|
131
165
|
##
|
132
|
-
# Returns a
|
133
|
-
#
|
166
|
+
# Returns a Cluster::ActiveRecord_Relation if the organization is using the cluster mode.
|
134
167
|
# Use this only to get current_user/organization clusters
|
135
|
-
# understanding this wont be useful out of a cluster mode context.
|
136
168
|
#
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
169
|
+
def cluster_of!(user,organization)
|
170
|
+
# A cluster user with role Root or Admin retrieve every cluster of its organizations
|
171
|
+
and_this_user = user.can_admin_clusters? ? nil : user
|
172
|
+
# We weill return a cluster active record collection that can be filtered
|
173
|
+
active_record_collection = ::ClusterUser.clusters_of(organization,and_this_user)
|
174
|
+
# Add a empty cluster instance to handle models without a cluster assignation.
|
175
|
+
if organization.optional_mode? # For use and show modes only
|
176
|
+
active_record_collection << ::Cluster.new({nid: UNCLUSTERED_ENTITY, name: I18n.t("klastera.#{UNCLUSTERED_ENTITY}")})
|
143
177
|
end
|
144
|
-
|
178
|
+
active_record_collection
|
145
179
|
end
|
146
|
-
end
|
147
180
|
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
hide_action :cluster_scope
|
161
|
-
hide_action :cluster_scope=
|
162
|
-
hide_action :user_cluster
|
163
|
-
hide_action :user_cluster=
|
164
|
-
hide_action :cluster_user
|
165
|
-
hide_action :cluster_user=
|
166
|
-
hide_action :cluster_organization
|
167
|
-
hide_action :cluster_organization=
|
168
|
-
hide_action :cluster_clusters
|
169
|
-
hide_action :cluster_clusters=
|
170
|
-
hide_action :cluster_scope_filtered
|
171
|
-
hide_action :cluster_scope_filtered=
|
172
|
-
hide_action :user_has_more_than_one_cluster
|
173
|
-
hide_action :user_has_more_than_one_cluster=
|
174
|
-
hide_action :clusters_from
|
175
|
-
hide_action :clusters_from=
|
181
|
+
##
|
182
|
+
# Return a string with cluster attribute separated by separator argument
|
183
|
+
# A array of cluster ids can be passed fo filter the result
|
184
|
+
#
|
185
|
+
def entity_clusters_string_list!(cluster_entities,separator,attribute=:name,allowed_cluster_ids=nil)
|
186
|
+
_cluster_entities = cluster_entities.reject(&:nil?)
|
187
|
+
if allowed_cluster_ids.is_a?(Array)
|
188
|
+
_cluster_entities.select!{|ce| allowed_cluster_ids.include?(ce.cluster_id)}
|
189
|
+
end
|
190
|
+
_cluster_entities.map do |ce|
|
191
|
+
ce.cluster.try(attribute)
|
192
|
+
end.compact.join(separator)
|
176
193
|
end
|
177
|
-
before_action :set_the_lonely_cluster, only: %i[ create update ]
|
178
|
-
end
|
179
194
|
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
195
|
+
##
|
196
|
+
# cluster_of! needs a user and a organization. that why we perfomed this logic here
|
197
|
+
#
|
198
|
+
def user_clusters_string_list!(user,organization,cluster_entities,separator,attribute=:name)
|
199
|
+
@clusters_session ||= self.cluster_of!(user,organization)
|
200
|
+
self.entity_clusters_string_list!(cluster_entities, separator, attribute, @clusters_session.map(&:id))
|
186
201
|
end
|
187
202
|
end
|
188
203
|
|
189
|
-
def cluster_scope(scope,includes=[])
|
190
|
-
Klastera.cluster_scope!(cluster_user,cluster_organization,scope,includes)
|
191
|
-
end
|
192
|
-
|
193
204
|
def cluster_user
|
194
205
|
current_user
|
195
206
|
end
|
@@ -198,27 +209,18 @@ module Klastera
|
|
198
209
|
current_organization
|
199
210
|
end
|
200
211
|
|
201
|
-
def cluster_clusters
|
202
|
-
Klastera.session_clusters(cluster_user,cluster_organization)
|
203
|
-
end
|
204
|
-
|
205
|
-
def cluster_scope_filtered(scope,cluster_id,includes=[])
|
206
|
-
Klastera.cluster_scope_filtered!(
|
207
|
-
scope,
|
208
|
-
cluster_id,
|
209
|
-
cluster_user,
|
210
|
-
cluster_organization,
|
211
|
-
includes
|
212
|
-
)
|
213
|
-
end
|
214
|
-
|
215
212
|
def user_has_more_than_one_cluster
|
216
|
-
@
|
217
|
-
@
|
213
|
+
@clusters_session ||= cluster_of_my_own
|
214
|
+
@clusters_session.size > 1
|
218
215
|
end
|
219
216
|
|
220
|
-
def
|
221
|
-
|
217
|
+
def set_the_lonely_cluster
|
218
|
+
form_model = @form_record ? model_name_from_record_or_class(@form_record).param_key : params[:controller].singularize
|
219
|
+
parameters = params.require( form_model ) rescue nil
|
220
|
+
lonely_cluster = parameters.blank? ? false : parameters.permit( :lonely_cluster ).present?
|
221
|
+
if lonely_cluster
|
222
|
+
params[form_model][:cluster_id] = cluster_of_my_own.first.try(:id)
|
223
|
+
end
|
222
224
|
end
|
223
225
|
|
224
226
|
def set_cluster_filter
|
@@ -234,4 +236,47 @@ module Klastera
|
|
234
236
|
def cluster_filter_permit_params
|
235
237
|
[ :cluster_id ].concat( ::ClusterFilter.attributes )
|
236
238
|
end
|
239
|
+
|
240
|
+
def filter_clusterized(active_record_collection,cluster_id)
|
241
|
+
Klastera.filter_clusterized!(active_record_collection, cluster_id, cluster_organization)
|
242
|
+
end
|
243
|
+
|
244
|
+
def cluster_scope(scope,cluster_id=nil)
|
245
|
+
Klastera.cluster_scope!(scope, cluster_user, cluster_organization, cluster_id)
|
246
|
+
end
|
247
|
+
|
248
|
+
def __cluster_scope(scope,cluster_id=nil,same_scope:false)
|
249
|
+
Klastera.__cluster_scope!(scope, cluster_user, cluster_organization, cluster_id, same_scope: same_scope)
|
250
|
+
end
|
251
|
+
|
252
|
+
def cluster_of_my_own
|
253
|
+
Klastera.cluster_of!(cluster_user, cluster_organization)
|
254
|
+
end
|
255
|
+
|
256
|
+
def user_clusters_string_list(object_entity,separator,attribute=:name)
|
257
|
+
Klastera.user_clusters_string_list!(
|
258
|
+
cluster_user,
|
259
|
+
cluster_organization,
|
260
|
+
object_entity.try(:cluster_entities),
|
261
|
+
separator,
|
262
|
+
attribute
|
263
|
+
)
|
264
|
+
end
|
265
|
+
|
266
|
+
def set_collection_before_group_by_entity(active_record_collection,entity_params,&block)
|
267
|
+
Klastera.set_collection_before_group_by_entity!(active_record_collection, params, cluster_organization, &block)
|
268
|
+
end
|
269
|
+
|
270
|
+
included do
|
271
|
+
Klastera::KLSTR_HELPERS.each do |action|
|
272
|
+
if respond_to?(:helper_method)
|
273
|
+
helper_method(action)
|
274
|
+
end
|
275
|
+
if respond_to?(:hide_action)
|
276
|
+
hide_action(helper)
|
277
|
+
hide_action("#{helper}=")
|
278
|
+
end
|
279
|
+
end
|
280
|
+
before_action :set_the_lonely_cluster, only: %i[ create update ]
|
281
|
+
end
|
237
282
|
end
|
data/lib/klastera/version.rb
CHANGED
@@ -1,3 +1,3 @@
|
|
1
1
|
module Klastera
|
2
|
-
VERSION = "1.
|
3
|
-
end
|
2
|
+
VERSION = "1.3.1"
|
3
|
+
end
|
@@ -13,6 +13,11 @@ namespace :klastera do
|
|
13
13
|
klass = args.entity.constantize
|
14
14
|
ActiveRecord::Base.transaction do
|
15
15
|
klass.where.not(cluster_id: nil).each do |entity|
|
16
|
+
if entity.cluster.blank?
|
17
|
+
puts "Cluster ID #{entity.cluster_id} was not found!"
|
18
|
+
puts "skip..."
|
19
|
+
next
|
20
|
+
end
|
16
21
|
Klastera::ClusterEntity.create(entity: entity, cluster: entity.cluster)
|
17
22
|
end
|
18
23
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: klastera
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.3.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Gino Barahona
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-06-
|
11
|
+
date: 2020-06-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -54,7 +54,6 @@ files:
|
|
54
54
|
- app/controllers/klastera/application_controller.rb
|
55
55
|
- app/controllers/klastera/clusters_controller.rb
|
56
56
|
- app/helpers/klastera/application_helper.rb
|
57
|
-
- app/helpers/klastera/clusters_helper.rb
|
58
57
|
- app/models/klastera/cluster.rb
|
59
58
|
- app/models/klastera/cluster_entity.rb
|
60
59
|
- app/models/klastera/cluster_filter.rb
|