Class: Refine::Filter

Inherits:
Object
  • Object
show all
Includes:
ActiveModel::Callbacks, ActiveModel::Validations, Internationalized, Inspector, Stabilize, TracksPendingRelationshipSubqueries
Defined in:
app/models/refine/filter.rb

Defined Under Namespace

Modules: Internationalized

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Inspector

#has_duplicate_conditions?, #uses_and?, #uses_condition, #uses_condition_at_least, #uses_negative_clause?, #uses_or?

Methods included from Stabilize

#automatic_stable_id_generator, #create_stable_id, #make_stable_id_generator, #to_optional_stable_id

Methods included from TracksPendingRelationshipSubqueries

#add_pending_joins_relationship_subquery, #add_pending_relationship_subquery, #allow_pending_relationship_to_collapse, #commit_pending_relationship_subqueries, #commit_subset, #get_current_relationship, #get_pending_relationship_instance, #get_pending_relationship_item, #get_pending_relationship_subquery, #pending_relationship_subqueries, #pending_relationship_subquery_depth, #relationship_supports_collapsing, #release_pending_relationship, #set_pending_relationship, #set_pending_relationship_subquery_wrapper, #use_multiple_databases?

Constructor Details

#initialize(blueprint = nil, query_scope = nil) ⇒ Filter

Returns a new instance of Filter.



39
40
41
42
43
44
45
46
47
48
49
50
51
52
# File 'app/models/refine/filter.rb', line 39

def initialize(blueprint = nil, query_scope = nil)
  run_callbacks :initialize do
    # If using this in test mode, `blueprint` will be an instance of
    # `Blueprint` and the value must be extracted
    if blueprint.is_a? Blueprints::Blueprint
      blueprint = blueprint.to_array
    end
    @initial_query = query_scope
    @blueprint = blueprint
    @relation = initial_query
    @immediately_commit_pending_relationship_subqueries = false
    @@default_stabilizer = Refine::Stabilizers::UrlEncodedStabilizer
  end
end

Class Attribute Details

.default_condition_id ⇒ Object

Returns the value of attribute default_condition_id.



35
36
37
# File 'app/models/refine/filter.rb', line 35

def default_condition_id
  @default_condition_id
end

Instance Attribute Details

#blueprint ⇒ Object (readonly)

Returns the value of attribute blueprint.



24
25
26
# File 'app/models/refine/filter.rb', line 24

def blueprint
  @blueprint
end

#initial_query ⇒ Object (readonly)

Returns the value of attribute initial_query.



24
25
26
# File 'app/models/refine/filter.rb', line 24

def initial_query
  @initial_query
end

Class Method Details

.default_stable_id_generator(klass) ⇒ Object



298
299
300
301
302
303
304
# File 'app/models/refine/filter.rb', line 298

def self.default_stable_id_generator(klass)
  if klass.method_defined?(:to_stable_id) && klass.method_defined?(:from_stable_id)
    @@default_stabilizer = klass
  else
    raise ArgumentError.new('Given class doesn\'t implement to_stable_id and from_stable_id!')
  end
end

.from_state(state, initial_query = nil) ⇒ Object



293
294
295
296
# File 'app/models/refine/filter.rb', line 293

def self.from_state(state, initial_query = nil)
  klass = state[:type].constantize
  filter = klass.new(state[:blueprint], initial_query)
end

.model ⇒ Object



17
18
19
# File 'app/models/refine/filter.rb', line 17

def self.model
  @model ||= model_name.constantize
end

.model_name ⇒ Object



13
14
15
# File 'app/models/refine/filter.rb', line 13

def self.model_name
  name.sub(/(::)?Filter$/, "").singularize
end

Instance Method Details

#add_nodes_to_query(subquery:, nodes:, query_method:) ⇒ Object



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

def add_nodes_to_query(subquery:, nodes:, query_method:)
  # Apply existing nodes to existing subquery
  if subquery.present? && nodes.present?
    subquery = if query_method == "and"
      # Apply the nodes using the AREL AND method
      subquery.send(query_method, group(nodes))
    else
      # Override the AREL OR method in order to remove the automatic parens
      Arel::Nodes::Or.new(subquery, group(nodes))
    end
  # No nodes returned, do nothing
  elsif subquery.present? && nodes.blank?
    subquery
  # Subquery has not yet been initialized, initialize with new nodes - must use !nil? here, present/exists/blank etc don't
  # account for AR::Relation object.
  elsif subquery.blank? && !nodes.nil?
    subquery = group(nodes)
  end
  subquery
end

#apply_condition(criterion) ⇒ Object



221
222
223
224
225
226
227
228
229
230
# File 'app/models/refine/filter.rb', line 221

def apply_condition(criterion)
  begin
    condition = get_condition_for_criterion(criterion)
    get_condition_for_criterion(criterion)&.apply(criterion[:input], table, initial_query, false, nil)
  rescue Refine::Conditions::Errors::ConditionClauseError => e
    e.errors.each do |error|
      errors.add(:base, error.full_message, criterion_uid: criterion[:uid])
    end
  end
end

#automatically_stabilize? ⇒ Boolean

Returns:

  • (Boolean)


74
75
76
# File 'app/models/refine/filter.rb', line 74

def automatically_stabilize?
  true
end

#blueprint_criteria ⇒ Object



310
311
312
# File 'app/models/refine/filter.rb', line 310

def blueprint_criteria
  blueprint&.filter { |node| node.is_a?(Hash) && node[:type] == 'criterion' }.to_a
end

#clear_blueprint! ⇒ Object



330
331
332
# File 'app/models/refine/filter.rb', line 330

def clear_blueprint!
  @blueprint = []
end

#conditions_to_array ⇒ Object



256
257
258
259
260
# File 'app/models/refine/filter.rb', line 256

def conditions_to_array
  return nil unless conditions
  # Set filter object on condition and return to_array
  conditions.map { |condition| instantiate_condition(condition) }.map(&:to_array)
end

#configuration ⇒ Object



246
247
248
249
250
251
252
253
254
# File 'app/models/refine/filter.rb', line 246

def configuration
  {
    type: "Refine",
    class_name: self.class.name,
    blueprint: @blueprint,
    conditions: conditions_to_array,
    stable_id: to_optional_stable_id
  }
end

#criteria_limit_exceeded? ⇒ Boolean

Returns:

  • (Boolean)


314
315
316
# File 'app/models/refine/filter.rb', line 314

def criteria_limit_exceeded?
  criteria_limit_set? && blueprint_criteria.length > criteria_limit
end

#criteria_limit_reached? ⇒ Boolean

Returns:

  • (Boolean)


318
319
320
# File 'app/models/refine/filter.rb', line 318

def criteria_limit_reached?
  criteria_limit_set? && blueprint_criteria.length >= criteria_limit
end

#criteria_limit_set? ⇒ Boolean

Returns:

  • (Boolean)


322
323
324
# File 'app/models/refine/filter.rb', line 322

def criteria_limit_set?
  criteria_limit.to_i.positive?
end

#fast_forward(index, modified_blueprint, depth) ⇒ Object



208
209
210
211
212
213
214
215
# File 'app/models/refine/filter.rb', line 208

def fast_forward(index, modified_blueprint, depth)
  fast_forward_index = (index..modified_blueprint.length - 1).each do |cursor|
    break cursor if modified_blueprint[cursor][:depth] <= depth
  end
  # TODO refactor for clarity. If I break early from the iterator I have an int. If not,
  # default to modfied_blueprint.length -1 as the correct value
  (fast_forward_index.is_a? Integer) ? fast_forward_index : modified_blueprint.length - 1
end

#get_complex_query ⇒ Object



107
108
109
110
111
112
113
# File 'app/models/refine/filter.rb', line 107

def get_complex_query
  if blueprint.present?
    @relation.where(group(make_sub_query(blueprint)))
  else
    @relation
  end
end

#get_condition_for_criterion(criterion) ⇒ Object



232
233
234
235
236
237
238
239
240
241
242
243
244
# File 'app/models/refine/filter.rb', line 232

def get_condition_for_criterion(criterion)
  # Returns the object that matches the condition. Adds errors if not found.
  # This checks the id on the condition such as text_test
  returned_object = conditions.find { |condition| condition.id == criterion[:condition_id] }
  if returned_object.nil?
    errors.add(:filter, "The condition ID #{criterion[:condition_id]} was not found")
  else
    # Set filter variable on condition
    instantiate_condition(returned_object)
  end
  # Must duplicate the condition so nested attributes don't bleed into one another
  returned_object.dup
end

#get_query ⇒ Object



92
93
94
95
96
97
98
99
# File 'app/models/refine/filter.rb', line 92

def get_query
  raise "Initial query must exist" if initial_query.nil?
  if self.class.included_modules.include?(Refine::FlatQueryTools) && can_use_flat_query?
    get_flat_query
  else
    get_complex_query
  end
end

#get_query! ⇒ Object



101
102
103
104
105
# File 'app/models/refine/filter.rb', line 101

def get_query!
  result = get_query
  raise Refine::InvalidFilterError.new(filter: self) unless errors.none?
  result
end

#group(nodes) ⇒ Object



217
218
219
# File 'app/models/refine/filter.rb', line 217

def group(nodes)
  Arel::Nodes::Grouping.new(nodes)
end

#has_category_ordering? ⇒ Boolean

Returns:

  • (Boolean)


326
327
328
# File 'app/models/refine/filter.rb', line 326

def has_category_ordering?
  respond_to?(:category_order) && category_order.is_a?(Array) && category_order.any?
end

#human_readable_criterions ⇒ Object



54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# File 'app/models/refine/filter.rb', line 54

def human_readable_criterions
  output = []
  if blueprint.present?
    blueprint.each do |criterion|
      if criterion[:type] == "conjunction"
        output << criterion[:word]
      else
        condition = get_condition_for_criterion(criterion)
        text = condition.human_readable(criterion[:input])
        if condition.has_date_refinement?
          date_refinement_condition = condition.has_date_refinement?.call
          text += date_refinement_condition.human_readable(criterion[:input][:date_refinement])
        end
        output << text
      end
    end
  end
  output
end

#instantiate_condition(condition) ⇒ Object



262
263
264
265
266
# File 'app/models/refine/filter.rb', line 262

def instantiate_condition(condition)
  condition.set_filter(self)
  translate_display(condition)
  condition
end

#instantiated_conditions ⇒ Object

Set filter object on condition and sort alphabetically



269
270
271
272
273
# File 'app/models/refine/filter.rb', line 269

def instantiated_conditions
  conditions
    .map { |c| instantiate_condition(c.dup) }
    .sort_by { |c| c.display.to_s.downcase }
end

#make_sub_query(modified_blueprint, depth = 0, subquery = nil) ⇒ Object



136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
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
199
200
201
202
203
204
205
206
# File 'app/models/refine/filter.rb', line 136

def make_sub_query(modified_blueprint, depth = 0, subquery = nil)
  # Need index control to directly skip indicies in fast forward (recursive call)
  index = 0
  while index < modified_blueprint.length
    criterion = modified_blueprint[index]
    # Decreasing depth, pass control back to caller
    break if criterion[:depth] < depth

    # If it's a conjunction, the next condition will handle it.
    if criterion[:type] == "conjunction"
      index += 1
      next
    end

    # Check the word on the previous blueprint method. If it is not 'and'....?
    query_method = if index == 0
      "and"
    else
      modified_blueprint[index - 1][:word] == "and" ? "and" : "or"
    end

    # If the new depth is deeper than our current depth, that means we're
    # starting a new group. We'll recursively call this method again
    # with a subset of the blueprint.
    if criterion[:depth] > depth
      # Modify the array to send in the elements not yet handled (depth>current depth)
      new_depth_array = modified_blueprint[index..-1]

      # Return the nodes in () for elements on the same depth
      recursive_nodes = make_sub_query(new_depth_array, depth + 1)

      # Add the recursive subquery nodes to the existing query and modify the query
      subquery = add_nodes_to_query(subquery: subquery, nodes: recursive_nodes, query_method: query_method)

      # Skip indexes handled by recursive call
      index = fast_forward(index, modified_blueprint, depth) + 1
      next
    end
    # If there are any ORs at this depth, commit subqueries
    @immediately_commit_pending_relationship_subqueries = if modified_blueprint.select { |item| (item[:type] == "conjunction" && item[:word] == "or" && item[:depth] == depth) }.present?
      true
    else
      false
    end

    # If it is a relationship attribute apply_condition will call apply_relationship_attribute which will set up the pending relationship
    # subquery but will not return a value.
    # apply condition is NOT idempotent, hence the placeholder var
    nodes_to_apply = apply_condition(criterion)
    # If an error has been added to the errors array from apply_condition, do not continue execution at this level
    if errors.any?
      index += 1
      next
    end

    subquery = add_nodes_to_query(subquery: subquery, nodes: nodes_to_apply, query_method: query_method)

    if @immediately_commit_pending_relationship_subqueries.present?
      committed_nodes_from_pending = commit_pending_relationship_subqueries
      subquery = add_nodes_to_query(subquery: subquery, nodes: committed_nodes_from_pending, query_method: query_method)
    end

    index += 1
  end

  unless errors.any?
    final_depth_nodes = commit_pending_relationship_subqueries
    # Add nodes to existing query and return existing query
    add_nodes_to_query(subquery: subquery, nodes: final_depth_nodes, query_method: query_method)
  end
end

#model ⇒ Object

e.g. ContactsFilter -> Contact



79
80
81
# File 'app/models/refine/filter.rb', line 79

def model
  initial_query&.model || self.class.model
end

#state ⇒ Object



282
283
284
285
286
287
# File 'app/models/refine/filter.rb', line 282

def state
  {
    type: type,
    blueprint: blueprint
  }.to_json
end

#table ⇒ Object



83
84
85
# File 'app/models/refine/filter.rb', line 83

def table
  model.arel_table
end

#to_stable_id ⇒ Object



306
307
308
# File 'app/models/refine/filter.rb', line 306

def to_stable_id
   Refine::Rails.configuration.stabilizer_classes[:url].new.to_stable_id(filter: self)
end

#translate_display(condition) ⇒ Object



275
276
277
278
279
280
# File 'app/models/refine/filter.rb', line 275

def translate_display(condition)
  # If there are no locale definitions for this condition's subject, we can allow I18n to use a human-readable version of the ID.
  # But, ideally, they have locales defined and we can find one of those.
  label_fallback = {default: condition.id.humanize(keep_id_suffix: true).titleize}
  condition.display = condition.display || I18n.t(".filter.conditions.#{condition.id}.label", default: I18n.t(".fields.#{condition.id}.label", **label_fallback))
end

#type ⇒ Object



289
290
291
# File 'app/models/refine/filter.rb', line 289

def type
  self.class.name
end

#valid_query? ⇒ Boolean

Returns:

  • (Boolean)


87
88
89
90
# File 'app/models/refine/filter.rb', line 87

def valid_query?
  get_query
  errors.none?
end