Module: Klastera

Extended by:
ActiveSupport::Concern
Defined in:
lib/klastera.rb,
lib/klastera/engine.rb,
lib/klastera/version.rb,
app/models/klastera/cluster.rb,
app/models/klastera/transfer.rb,
app/models/klastera/cluster_user.rb,
app/models/klastera/cluster_entity.rb,
app/models/klastera/cluster_filter.rb,
app/helpers/klastera/application_helper.rb,
app/controllers/klastera/clusters_controller.rb,
app/controllers/klastera/application_controller.rb

Defined Under Namespace

Modules: ApplicationHelper Classes: ApplicationController, Cluster, ClusterEntity, ClusterFilter, ClusterUser, ClustersController, Engine, Transfer

Constant Summary collapse

UNCLUSTERED_POSITION =
9999
UNCLUSTERED_ENTITY =
'without_cluster'.freeze
KLSTR_HELPERS =
%i[ cluster_user cluster_organization cluster_list cluster_scope cluster_scope_through_of user_clusters_string_list cluster_scope_left_join ].freeze
VERSION =
"1.5.5.1"

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.cluster_list!(organization, user, include_unclustered = true) ⇒ Object

Returns which clusters a user can see avoiding unnecessary queries if the cluster restraint doesn’t apply



28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# File 'lib/klastera.rb', line 28

def cluster_list!(organization,user,include_unclustered=true)
  # Only the cluster mode on and the mandatory user-cluster relation will use a join clause to get the clusters list
  if organization.is_in_cluster_mode? && user.cannot_skip_cluster_clause?
    active_record_collection = ::ClusterUser.clusters_of(organization,user)
  else
    active_record_collection = ::Cluster.where({ organization_id: organization }).order(order: :asc)
  end

  active_record_collection = active_record_collection.order(order: :asc)

  if include_unclustered && organization.optional_suborganization_mode? # For show and use modes only
    active_record_collection.to_a.append(
      ::Cluster.new({nid: UNCLUSTERED_ENTITY, name: I18n.t("klastera.#{UNCLUSTERED_ENTITY}"), order: UNCLUSTERED_POSITION })
    )
  end
  active_record_collection
end

.cluster_scope!(scope_klass, user, organization, cluster_filter = nil, force_cluster_clause = false) ⇒ Object

The cleanest and fast way to clusterize a entity!



140
141
142
143
144
145
146
147
148
# File 'lib/klastera.rb', line 140

def cluster_scope!(scope_klass, user, organization, cluster_filter=nil, force_cluster_clause=false)
  scope = scope_class(scope_klass)
  should_clusterize_scope?(user,organization,cluster_filter,force_cluster_clause) do |should,cluster_ids|
    if should
      scope = scope.eager_load(:organization,cluster_entities: :cluster).where( cluster_entities: { cluster_id: cluster_ids } )
    end
  end
  scope.where(organization_id: organization)
end

.cluster_scope_left_join!(scope_klass, organization) ⇒ Object

A helper that returns a CLUSTER SCOPE to build queries that need explicit LEFT OUTER JOIN clause, instead of the default INNER JOIN provide by ActiveRecord’s joins method



204
205
206
207
208
209
210
211
212
# File 'lib/klastera.rb', line 204

def cluster_scope_left_join!(scope_klass,organization)
  cluster_entities_arel_table = Klastera::ClusterEntity.arel_table
  cluster_arel_table = ::Cluster.arel_table
  cluster_entities_cluster = cluster_entities_arel_table.join(cluster_arel_table, Arel::Nodes::OuterJoin).on(
    cluster_entities_arel_table[:cluster_id].eq(cluster_arel_table[:id]),
  ).join_sources

  scope_class(scope_klass).where(organization_id: organization).joins(Klastera::ClusterEntity.left_join_sources_of(scope_klass)).joins(cluster_entities_cluster)
end

.cluster_scope_through_of!(relation, cluster_entity_klass, scope_klass, user, organization, cluster_filter = nil, force_cluster_clause = false) ⇒ Object

 Filter non-clustered entity through a clusterized one



153
154
155
156
157
158
159
160
161
162
163
164
165
# File 'lib/klastera.rb', line 153

def cluster_scope_through_of!(relation, cluster_entity_klass, scope_klass, user, organization, cluster_filter=nil, force_cluster_clause=false)
  unclusterized_scope = scope_class(scope_klass)

  if organization.is_in_cluster_mode? && ( force_cluster_clause || user.cannot_skip_cluster_clause? )
    unclusterized_scope = unclusterized_scope.joins(relation).joins(Klastera::ClusterEntity.left_join_sources_of(cluster_entity_klass))
  end

  if scope_klass.respond_to?(:organization)
    unclusterized_scope = unclusterized_scope.where(organization_id: organization)
  end

  unclusterized_scope.where("#{relation}_id" => cluster_scope!(cluster_entity_klass, user, organization, cluster_filter, force_cluster_clause))
end

.entity_clusters_string_list!(cluster_entities, separator, attribute = :name, allowed_cluster_ids = nil) ⇒ Object

Return a string with cluster attribute separated by separator argument A array of cluster ids can be passed fo filter the result Optimized version that uses preloaded associations when available



51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/klastera.rb', line 51

def entity_clusters_string_list!(cluster_entities,separator,attribute=:name,allowed_cluster_ids=nil)
  _cluster_entities = cluster_entities.reject(&:nil?)
  if allowed_cluster_ids.is_a?(Array)
    _cluster_entities.select!{|ce| allowed_cluster_ids.include?(ce.cluster_id)}
  end

  # Performance optimization: Use preloaded data when available
  if cluster_entities.respond_to?(:loaded?) && cluster_entities.loaded? &&
    _cluster_entities.all? { |ce| ce.association(:cluster).loaded? }
    # Use in-memory data (much faster)
    _cluster_entities.map do |ce|
      ce.cluster.try(attribute)
    end.compact.sort.join(separator)
  else
    # Fallback to original method with database query
    # But use a single query instead of N+1

    cluster_ids = _cluster_entities.filter_map(&:cluster_id).uniq
    return "" if cluster_ids.empty?

    records = ::Cluster.where(id: cluster_ids).pluck(:id, attribute)
    return "" if records.empty?

    # clusters_hash = if records.any? && !records.first.is_a?(Array)
    #   records.map { |id| [id, id] }.to_h
    # else
    #   records.to_h
    # end
    # _cluster_entities.map do |ce|
    #   clusters_hash[ce.cluster_id]
    # end.compact.sort.join(separator)

    clusters_hash = records.first.is_a?(Array) ? records.to_h : records.index_by(&:itself)
    _cluster_entities
      .filter_map { |entity| clusters_hash[entity.cluster_id] }
      .uniq
      .sort
      .join(separator)

  end
end

.group_by_cluster_scope!(scope_klass, user, organization, cluster_filter = [], scope_scopes = []) ⇒ Object

Returns an array with a clusterized scoped result and its grouped version



170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
# File 'lib/klastera.rb', line 170

def group_by_cluster_scope!(scope_klass, user, organization, cluster_filter=[], scope_scopes=[])
  cluster_ids = cluster_filter.is_a?(Array) ? cluster_filter : [cluster_filter]
  kluster_scope = cluster_scope!(scope_klass, user, organization, cluster_ids.compact, organization.is_in_cluster_mode? )

  scope_scopes.each do |tuple_scope|
    scope_name, scope_arg = tuple_scope
    kluster_scope = scope_arg.present? ? kluster_scope.send(scope_name,scope_arg) : kluster_scope.send(scope_name)
  end

  group_by_block = ->(o) {
    if organization.is_in_cluster_mode?
      o.cluster.present? ? o.cluster.name : UNCLUSTERED_POSITION
    else
      I18n.t("klastera.group_by_cluster_scope.#{scope_klass.model_name.plural}")
    end
  }

  grouped_cluster_scope = kluster_scope.group_by(&group_by_block).sort_by{|k,v|k.to_s}

  grouped_cluster_scope.dup.each do |group|
    if group.first == UNCLUSTERED_POSITION
      grouped_cluster_scope.delete(group)
      group[0] = I18n.t("klastera.#{UNCLUSTERED_ENTITY}")
      grouped_cluster_scope.append(group)
    end
  end

  [ kluster_scope, grouped_cluster_scope ]
end

.scope_class(object) ⇒ Object

TODO: Implement a validation to ensure that  object is a ActiveRecord::Base class (or just try to guess how to retrieve the argument class)



21
22
23
# File 'lib/klastera.rb', line 21

def scope_class(object)
  object
end

.should_clusterize_scope?(user, organization, cluster_filter = nil, force_cluster_clause = false) {|should, cluster_ids| ... } ⇒ Boolean

We will try to avoid cluster clause except when: 1.- cluster mode is active AND

2a.- cluster_filter is present (someone wants to filter by cluster)

OR

2b.- the current user has some limitations and must checks they cluster relation
      - User is having clusters in optional_suborganization mode
      - User IS NOT having clusters in required_suborganization mode

For the other hand, with force_cluster_clause we can skip the previous logic if cluster_filter_id is present when the optional_suborganization mode is on. BUT! Be aware that if the cluster_filter is not present, the value of force_cluster_clause will be overridden by the returned value of cannot_skip_cluster_clause? method.

Yields:

  • (should, cluster_ids)

Returns:

  • (Boolean)


115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
# File 'lib/klastera.rb', line 115

def should_clusterize_scope?(user, organization, cluster_filter=nil, force_cluster_clause=false)
  should = false # I don't know if this is a good idea
  if organization.is_in_cluster_mode? && ( cluster_filter.present? || force_cluster_clause = user.cannot_skip_cluster_clause? ) # yes, this is an assignation
    cluster_ids = []
    # Set another variable as array to get the cluster id(s)
    if cluster_filter.present?
      cluster_ids = cluster_filter.is_a?(Array) ? cluster_filter : [cluster_filter]
    elsif force_cluster_clause
      cluster_ids = ::ClusterUser.clusters_of(organization,user).map(&:id)
    end
    # We will avoid the query unless cluster_ids is having values OR force_cluster_clause is set (see method description)
    if cluster_ids.present? || force_cluster_clause
      # We add the unclustered if the value of cluster_filter have the special without_cluster string or as method description says
      if cluster_ids.delete(UNCLUSTERED_ENTITY) || ( force_cluster_clause && organization.optional_suborganization_mode? )
        cluster_ids << nil
      end
      should = true
    end
  end
  yield(should,cluster_ids)
end

.user_clusters_string_list!(user, organization, cluster_entities, separator, attribute = :name) ⇒ Object

cluster_list! needs a user and a organization. that why we perfomed this logic here



96
97
98
99
# File 'lib/klastera.rb', line 96

def user_clusters_string_list!(user,organization,cluster_entities,separator,attribute=:name)
  @clusters_session ||= Klastera.cluster_list!(organization,user)
  self.entity_clusters_string_list!(cluster_entities, separator, attribute, @clusters_session.map(&:id))
end

Instance Method Details

#cluster_list(include_unclustered = true) ⇒ Object



234
235
236
# File 'lib/klastera.rb', line 234

def cluster_list(include_unclustered=true)
  Klastera.cluster_list!(cluster_organization, cluster_user, include_unclustered)
end

#cluster_organizationObject



221
222
223
# File 'lib/klastera.rb', line 221

def cluster_organization
  current_organization
end

#cluster_scope(scope_klass, cluster_filter = nil, force_cluster_clause = false) ⇒ Object



238
239
240
# File 'lib/klastera.rb', line 238

def cluster_scope(scope_klass, cluster_filter=nil, force_cluster_clause=false)
  Klastera.cluster_scope!(scope_klass, cluster_user, cluster_organization, cluster_filter, force_cluster_clause)
end

#cluster_scope_left_join(scope_klass) ⇒ Object



254
255
256
# File 'lib/klastera.rb', line 254

def cluster_scope_left_join(scope_klass)
  Klastera.cluster_scope_left_join!(scope_klas,cluster_organization)
end

#cluster_scope_through_of(relation, cluster_entity_klass, scope_klass, cluster_filter = nil, force_cluster_clause = false) ⇒ Object



242
243
244
# File 'lib/klastera.rb', line 242

def cluster_scope_through_of(relation, cluster_entity_klass, scope_klass, cluster_filter=nil, force_cluster_clause=false)
  Klastera.cluster_scope_through_of!(relation, cluster_entity_klass, scope_klass, cluster_user, cluster_organization, cluster_filter, force_cluster_clause)
end

#cluster_userObject



217
218
219
# File 'lib/klastera.rb', line 217

def cluster_user
  current_user
end

#group_by_cluster_scope(scope_klass, cluster_filter = [], scope_scopes = []) ⇒ Object



246
247
248
# File 'lib/klastera.rb', line 246

def group_by_cluster_scope(scope_klass, cluster_filter=[], scope_scopes=[])
  Klastera.group_by_cluster_scope!(scope_klass, cluster_user, cluster_organization, cluster_filter, scope_scopes)
end

#set_cluster_filterObject



225
226
227
228
229
230
231
232
# File 'lib/klastera.rb', line 225

def set_cluster_filter
  cluster_filter_params = params.require(:cluster_filter) rescue {}
  @cluster_filter = ::ClusterFilter.new(
    cluster_filter_params.present? ? cluster_filter_params.permit(
      [ :cluster_id ].concat( ::ClusterFilter.attributes )
    ) : {}
  )
end

#user_clusters_string_list(object_entity, separator, attribute = :name) ⇒ Object



250
251
252
# File 'lib/klastera.rb', line 250

def user_clusters_string_list(object_entity, separator, attribute=:name)
  Klastera.user_clusters_string_list!(cluster_user, cluster_organization, object_entity.try(:cluster_entities), separator, attribute)
end