refine-rails 2.9.0
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 +7 -0
- data/README.md +413 -0
- data/Rakefile +8 -0
- data/app/assets/config/refine_rails_manifest.js +0 -0
- data/app/assets/javascripts/refine-stimulus.esm.js +2 -0
- data/app/assets/javascripts/refine-stimulus.esm.js.map +1 -0
- data/app/assets/javascripts/refine-stimulus.js +2 -0
- data/app/assets/javascripts/refine-stimulus.js.map +1 -0
- data/app/assets/javascripts/refine-stimulus.modern.js +2 -0
- data/app/assets/javascripts/refine-stimulus.modern.js.map +1 -0
- data/app/assets/javascripts/refine-stimulus.umd.js +2 -0
- data/app/assets/javascripts/refine-stimulus.umd.js.map +1 -0
- data/app/assets/stylesheets/index.css +1873 -0
- data/app/assets/stylesheets/index.tailwind.css +1035 -0
- data/app/controllers/refine/blueprints_controller.rb +80 -0
- data/app/controllers/refine/filter_application_controller.rb +29 -0
- data/app/controllers/refine/inline/criteria_controller.rb +161 -0
- data/app/controllers/refine/inline/stored_filters_controller.rb +84 -0
- data/app/controllers/refine/stored_filters_controller.rb +69 -0
- data/app/javascript/controllers/index.js +66 -0
- data/app/javascript/controllers/refine/add-controller.js +42 -0
- data/app/javascript/controllers/refine/criterion-form-controller.js +31 -0
- data/app/javascript/controllers/refine/date-controller.js +113 -0
- data/app/javascript/controllers/refine/defaults-controller.js +32 -0
- data/app/javascript/controllers/refine/delete-controller.js +13 -0
- data/app/javascript/controllers/refine/filter-pills-controller.js +63 -0
- data/app/javascript/controllers/refine/form-controller.js +51 -0
- data/app/javascript/controllers/refine/inline-conditions-controller.js +33 -0
- data/app/javascript/controllers/refine/popup-controller.js +46 -0
- data/app/javascript/controllers/refine/search-filter-controller.js +50 -0
- data/app/javascript/controllers/refine/server-refresh-controller.js +43 -0
- data/app/javascript/controllers/refine/state-controller.js +220 -0
- data/app/javascript/controllers/refine/stored-filter-controller.js +23 -0
- data/app/javascript/controllers/refine/submit-form-controller.js +8 -0
- data/app/javascript/controllers/refine/toggle-controller.js +12 -0
- data/app/javascript/controllers/refine/turbo-stream-form-controller.js +24 -0
- data/app/javascript/controllers/refine/turbo-stream-link-controller.js +24 -0
- data/app/javascript/controllers/refine/update-controller.js +86 -0
- data/app/javascript/index.js +1 -0
- data/app/javascript/refine/helpers/index.js +77 -0
- data/app/models/refine/blueprints/blueprint.rb +58 -0
- data/app/models/refine/blueprints/blueprint_example.json +25 -0
- data/app/models/refine/conditions/boolean_condition.rb +112 -0
- data/app/models/refine/conditions/clause.rb +38 -0
- data/app/models/refine/conditions/clauses.rb +38 -0
- data/app/models/refine/conditions/condition.rb +285 -0
- data/app/models/refine/conditions/condition_error.rb +1 -0
- data/app/models/refine/conditions/date_condition.rb +464 -0
- data/app/models/refine/conditions/date_with_time_condition.rb +8 -0
- data/app/models/refine/conditions/errors/condition_clause_error.rb +7 -0
- data/app/models/refine/conditions/errors/criteria_limit_exceeded_error.rb +2 -0
- data/app/models/refine/conditions/errors/option_error.rb +2 -0
- data/app/models/refine/conditions/errors/relationship_error.rb +1 -0
- data/app/models/refine/conditions/filter_condition.rb +93 -0
- data/app/models/refine/conditions/has_clauses.rb +117 -0
- data/app/models/refine/conditions/has_meta.rb +10 -0
- data/app/models/refine/conditions/has_refinements.rb +156 -0
- data/app/models/refine/conditions/numeric_condition.rb +224 -0
- data/app/models/refine/conditions/option_condition.rb +260 -0
- data/app/models/refine/conditions/text_condition.rb +152 -0
- data/app/models/refine/conditions/uses_attributes.rb +168 -0
- data/app/models/refine/filter.rb +302 -0
- data/app/models/refine/filters/blueprint_editor.rb +102 -0
- data/app/models/refine/filters/builder.rb +59 -0
- data/app/models/refine/filters/criterion.rb +87 -0
- data/app/models/refine/filters/query.rb +82 -0
- data/app/models/refine/inline/criteria/input.rb +50 -0
- data/app/models/refine/inline/criteria/numeric_refinement.rb +13 -0
- data/app/models/refine/inline/criteria/option.rb +2 -0
- data/app/models/refine/inline/criterion.rb +141 -0
- data/app/models/refine/invalid_filter_error.rb +8 -0
- data/app/models/refine/stabilize.rb +29 -0
- data/app/models/refine/stabilizers/database_stabilizer.rb +21 -0
- data/app/models/refine/stabilizers/errors/url_stabilizer_error.rb +2 -0
- data/app/models/refine/stabilizers/url_encoded_stabilizer.rb +21 -0
- data/app/models/refine/stored_filter.rb +14 -0
- data/app/models/refine/tracks_pending_relationship_subqueries.rb +196 -0
- data/app/views/_filter_builder_dropdown.html.erb +63 -0
- data/app/views/_filter_pills.html.erb +40 -0
- data/app/views/_loading.html.erb +32 -0
- data/app/views/refine/blueprints/_add_and.html.erb +25 -0
- data/app/views/refine/blueprints/_add_group.html.erb +24 -0
- data/app/views/refine/blueprints/_clause_select.html.erb +24 -0
- data/app/views/refine/blueprints/_condition_select.html.erb +53 -0
- data/app/views/refine/blueprints/_criterion.html.erb +41 -0
- data/app/views/refine/blueprints/_criterion_errors.html.erb +7 -0
- data/app/views/refine/blueprints/_delete_criterion.html.erb +11 -0
- data/app/views/refine/blueprints/_group.html.erb +13 -0
- data/app/views/refine/blueprints/_query.html.erb +34 -0
- data/app/views/refine/blueprints/_stored_filters.html.erb +23 -0
- data/app/views/refine/blueprints/clauses/_date_condition.html.erb +80 -0
- data/app/views/refine/blueprints/clauses/_date_picker.html.erb +26 -0
- data/app/views/refine/blueprints/clauses/_filter_condition.html.erb +36 -0
- data/app/views/refine/blueprints/clauses/_numeric_condition.html.erb +35 -0
- data/app/views/refine/blueprints/clauses/_option_condition.html.erb +37 -0
- data/app/views/refine/blueprints/clauses/_text_condition.html.erb +13 -0
- data/app/views/refine/blueprints/create.turbo_stream.erb +22 -0
- data/app/views/refine/blueprints/new.html.erb +7 -0
- data/app/views/refine/blueprints/show.html.erb +4 -0
- data/app/views/refine/blueprints/show.turbo_stream.erb +22 -0
- data/app/views/refine/inline/criteria/_form_fields.html.erb +62 -0
- data/app/views/refine/inline/criteria/create.turbo_stream.erb +19 -0
- data/app/views/refine/inline/criteria/edit.turbo_stream.erb +26 -0
- data/app/views/refine/inline/criteria/index.html.erb +64 -0
- data/app/views/refine/inline/criteria/new.turbo_stream.erb +24 -0
- data/app/views/refine/inline/filters/_add_first_condition_button.html.erb +19 -0
- data/app/views/refine/inline/filters/_and_button.html.erb +26 -0
- data/app/views/refine/inline/filters/_criterion.html.erb +23 -0
- data/app/views/refine/inline/filters/_group.html.erb +13 -0
- data/app/views/refine/inline/filters/_load_button.html.erb +15 -0
- data/app/views/refine/inline/filters/_or_button.html.erb +26 -0
- data/app/views/refine/inline/filters/_popup.html.erb +26 -0
- data/app/views/refine/inline/filters/_save_button.html.erb +15 -0
- data/app/views/refine/inline/filters/_show.html.erb +40 -0
- data/app/views/refine/inline/inputs/_date_condition.html.erb +7 -0
- data/app/views/refine/inline/inputs/_date_condition_days.html.erb +18 -0
- data/app/views/refine/inline/inputs/_date_condition_range.html.erb +22 -0
- data/app/views/refine/inline/inputs/_date_condition_single.html.erb +9 -0
- data/app/views/refine/inline/inputs/_date_picker.html.erb +20 -0
- data/app/views/refine/inline/inputs/_numeric_condition.html.erb +23 -0
- data/app/views/refine/inline/inputs/_option_condition.html.erb +14 -0
- data/app/views/refine/inline/inputs/_text_condition.html.erb +8 -0
- data/app/views/refine/inline/stored_filters/find.turbo_stream.erb +19 -0
- data/app/views/refine/inline/stored_filters/index.html.erb +28 -0
- data/app/views/refine/inline/stored_filters/new.turbo_stream.erb +47 -0
- data/app/views/refine/stored_filters/create.turbo_stream.erb +2 -0
- data/app/views/refine/stored_filters/find.turbo_stream.erb +5 -0
- data/app/views/refine/stored_filters/index.html.erb +39 -0
- data/app/views/refine/stored_filters/new.html.erb +29 -0
- data/app/views/refine/stored_filters/show.html.erb +1 -0
- data/config/locales/en/dates.en.yml +29 -0
- data/config/locales/en/en.yml +20 -0
- data/config/locales/en/refine.en.yml +187 -0
- data/config/routes.rb +17 -0
- data/lib/generators/filter/filter_generator.rb +27 -0
- data/lib/generators/filter/templates/filter.rb.erb +20 -0
- data/lib/refine/rails/engine.rb +15 -0
- data/lib/refine/rails/version.rb +5 -0
- data/lib/refine/rails.rb +38 -0
- data/lib/tasks/refine/rails_tasks.rake +13 -0
- metadata +202 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
module Refine::Conditions
|
|
2
|
+
module HasRefinements
|
|
3
|
+
def refine_by_filter(filter_condition)
|
|
4
|
+
@filter_refinement_proc = proc { filter_condition }
|
|
5
|
+
self
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def refine_by_date(callable = nil)
|
|
9
|
+
# Developer can send in a string that represents the attribute or a proc that is a fully qualified class
|
|
10
|
+
# If a callable is given and it is a string, we assume that is the desired attribute. Otherwise we use
|
|
11
|
+
# the passed in callable. If no callable is given we create on based on standard date with time condition
|
|
12
|
+
@date_refinement_proc = if callable.present?
|
|
13
|
+
(callable.is_a? String) ? define_date_condition(attribute: callable) : callable
|
|
14
|
+
else
|
|
15
|
+
define_date_condition(attribute: "created_at")
|
|
16
|
+
end
|
|
17
|
+
self
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def define_date_condition(attribute:)
|
|
21
|
+
proc { DateCondition.new("date_refinement").with_attribute(attribute).attribute_is_date_with_time }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def refine_by_count(callable = nil)
|
|
25
|
+
@count_refinement_proc = callable.present? ? callable : define_count_condition
|
|
26
|
+
self
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def define_count_condition
|
|
30
|
+
proc { NumericCondition.new("count_refinement") }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def apply_refinements(input)
|
|
34
|
+
if has_any_refinements? && !refinements_allowed?
|
|
35
|
+
raise Errors::RelationshipError, I18n.t("refine.refine_blueprints.has_refinements.not_allowed")
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
instance = filter.get_pending_relationship_instance
|
|
39
|
+
|
|
40
|
+
subquery_table = instance.klass.arel_table
|
|
41
|
+
subquery = filter.get_pending_relationship_subquery
|
|
42
|
+
|
|
43
|
+
if @date_refinement_proc
|
|
44
|
+
nodes = apply_date_refinement(subquery_table, input[:date_refinement])
|
|
45
|
+
end
|
|
46
|
+
if @count_refinement_proc
|
|
47
|
+
apply_count_refinement(table: subquery_table, input: input[:count_refinement], subquery: subquery, instance: instance)
|
|
48
|
+
end
|
|
49
|
+
nodes
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def has_any_refinements?
|
|
53
|
+
!!(has_date_refinement? || has_count_refinement?)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def has_count_refinement?
|
|
57
|
+
@count_refinement_proc
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def has_date_refinement?
|
|
61
|
+
@date_refinement_proc
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def refinements_allowed?
|
|
65
|
+
instance = filter.get_pending_relationship_instance
|
|
66
|
+
(instance.is_a? ActiveRecord::Reflection::HasManyReflection) || (instance.is_a? ActiveRecord::Reflection::ThroughReflection)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def apply_date_refinement(table, input)
|
|
70
|
+
get_date_refinement_condition.apply(input, table, nil)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def apply_count_refinement(table:, input:, subquery:, instance:)
|
|
74
|
+
condition = get_count_refinement_condition
|
|
75
|
+
# We need to group by because we're going to be using a `having`
|
|
76
|
+
# to get the count of records. Since we're using a where in for this
|
|
77
|
+
# relationship subquery, we're only selecting the foreign key,
|
|
78
|
+
# so we'll just group by that too.
|
|
79
|
+
subquery.group(table[instance.foreign_key])
|
|
80
|
+
|
|
81
|
+
# If user's input doesn't include the number 0, there is no complicated
|
|
82
|
+
# joining
|
|
83
|
+
if condition.input_could_include_zero?(input)
|
|
84
|
+
# if the input DOES include 0 then we have to do a bit of extra work. If you were trying to get a list of
|
|
85
|
+
# contacts that has i.e. 0 events, you can't query the events table b/c those contacts
|
|
86
|
+
# don't have any record there.
|
|
87
|
+
# Get a list of all contacts, left join in the count of events we're looking for, and coalesce nulls
|
|
88
|
+
# to 0. That gives us a true count for every contact, even if the count is 0. Then we can use
|
|
89
|
+
# the Numeric Condition as usual.
|
|
90
|
+
# Wrap the existing subquery - subquery is an AREL Select manager and modified in place
|
|
91
|
+
subquery.project((Arel.star.count).as("hs_refine_count_aggregate"))
|
|
92
|
+
|
|
93
|
+
# The table that owns the relationship
|
|
94
|
+
# TODO This is redundant
|
|
95
|
+
parent_table = instance.active_record.arel_table
|
|
96
|
+
parent_primary_key = instance.active_record.primary_key
|
|
97
|
+
|
|
98
|
+
# SELECT "contacts"."id" FROM "contacts"
|
|
99
|
+
outer_query = parent_table.project(parent_table[parent_primary_key.to_sym])
|
|
100
|
+
|
|
101
|
+
callable_subquery =
|
|
102
|
+
proc do |inner_query, primary_key, foreign_key|
|
|
103
|
+
interim_table = inner_query.as("interim_table")
|
|
104
|
+
outer_query.join(interim_table, Arel::Nodes::OuterJoin).on(interim_table[foreign_key.to_sym].eq(parent_table[primary_key.to_sym]))
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
filter.set_pending_relationship_subquery_wrapper(callable_subquery)
|
|
108
|
+
|
|
109
|
+
condition.raw_attribute("coalesce(hs_refine_count_aggregate, 0)")
|
|
110
|
+
|
|
111
|
+
node = condition.apply(input, table, nil)
|
|
112
|
+
outer_query.where(node)
|
|
113
|
+
else
|
|
114
|
+
node = condition.apply(input, table, nil)
|
|
115
|
+
# Modify the pending relationship subquery in place to be applied later
|
|
116
|
+
# Remember, AREL in this instance has passed by reference so pending_relationship_subquery will be modified
|
|
117
|
+
subquery.having(node)
|
|
118
|
+
# Return nil case due to pending relationship subquery not yet being applied
|
|
119
|
+
nil
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def get_date_refinement_condition
|
|
124
|
+
# Create the condition from the callable
|
|
125
|
+
condition = @date_refinement_proc.call
|
|
126
|
+
# Overwrite any passed in id's with date_refinement
|
|
127
|
+
condition.id = "date_refinement"
|
|
128
|
+
condition.is_refinement = true
|
|
129
|
+
filter.instantiate_condition(condition)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def get_count_refinement_condition
|
|
133
|
+
condition = @count_refinement_proc.call
|
|
134
|
+
# Overwrite any passed in id's with count_refinement
|
|
135
|
+
condition.id = "count_refinement"
|
|
136
|
+
condition.is_refinement = true
|
|
137
|
+
condition.raw_attribute("COUNT(*)")
|
|
138
|
+
filter.instantiate_condition(condition)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def refinements_to_array
|
|
142
|
+
if is_refinement
|
|
143
|
+
[]
|
|
144
|
+
else
|
|
145
|
+
refinement_array = []
|
|
146
|
+
if @date_refinement_proc
|
|
147
|
+
refinement_array << get_date_refinement_condition.to_array
|
|
148
|
+
end
|
|
149
|
+
if @count_refinement_proc
|
|
150
|
+
refinement_array << get_count_refinement_condition.to_array
|
|
151
|
+
end
|
|
152
|
+
refinement_array
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
module Refine::Conditions
|
|
2
|
+
class NumericCondition < Condition
|
|
3
|
+
include ActiveModel::Validations
|
|
4
|
+
include HasClauses
|
|
5
|
+
|
|
6
|
+
cattr_accessor :default_clause_display_map, default: {}, instance_accessor: false
|
|
7
|
+
|
|
8
|
+
validates :value1, numericality: true, allow_nil: true
|
|
9
|
+
validates :value2, numericality: true, allow_nil: true
|
|
10
|
+
|
|
11
|
+
with_options if: :floats_not_allowed? do
|
|
12
|
+
validates :value1, numericality: {only_integer: true}, allow_nil: true
|
|
13
|
+
validates :value2, numericality: {only_integer: true}, allow_nil: true
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
attr_reader :value1, :value2
|
|
17
|
+
|
|
18
|
+
CLAUSE_EQUALS = Clauses::EQUALS
|
|
19
|
+
CLAUSE_DOESNT_EQUAL = Clauses::DOESNT_EQUAL
|
|
20
|
+
|
|
21
|
+
CLAUSE_LESS_THAN_OR_EQUAL = Clauses::LESS_THAN_OR_EQUAL
|
|
22
|
+
CLAUSE_LESS_THAN = Clauses::LESS_THAN
|
|
23
|
+
CLAUSE_GREATER_THAN = Clauses::GREATER_THAN
|
|
24
|
+
CLAUSE_GREATER_THAN_OR_EQUAL = Clauses::GREATER_THAN_OR_EQUAL
|
|
25
|
+
|
|
26
|
+
CLAUSE_BETWEEN = Clauses::BETWEEN
|
|
27
|
+
CLAUSE_NOT_BETWEEN = Clauses::NOT_BETWEEN
|
|
28
|
+
|
|
29
|
+
CLAUSE_SET = Clauses::SET
|
|
30
|
+
CLAUSE_NOT_SET = Clauses::NOT_SET
|
|
31
|
+
|
|
32
|
+
I18N_PREFIX = "refine.refine_blueprints.numeric_condition."
|
|
33
|
+
|
|
34
|
+
def boot
|
|
35
|
+
@floats = false
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def set_input_parameters(input)
|
|
39
|
+
@value1 = input[:value1]
|
|
40
|
+
@value2 = input[:value2]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def component
|
|
44
|
+
"numeric-condition"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def human_readable(input)
|
|
48
|
+
current_clause = get_clause_by_id(input[:clause])
|
|
49
|
+
case input[:clause]
|
|
50
|
+
when *[CLAUSE_EQUALS, CLAUSE_DOESNT_EQUAL, CLAUSE_GREATER_THAN, CLAUSE_GREATER_THAN_OR_EQUAL, CLAUSE_LESS_THAN, CLAUSE_LESS_THAN_OR_EQUAL]
|
|
51
|
+
"#{display} #{current_clause.display} #{input[:value1]}"
|
|
52
|
+
when *[CLAUSE_BETWEEN, CLAUSE_NOT_BETWEEN]
|
|
53
|
+
"#{display} #{current_clause.display} #{input[:value1]} #{I18n.t("#{I18N_PREFIX}and")} #{input[:value2]}"
|
|
54
|
+
when *[CLAUSE_SET, CLAUSE_NOT_SET]
|
|
55
|
+
"#{display} #{current_clause.display}"
|
|
56
|
+
else
|
|
57
|
+
raise "#{input[:clause]} #{I18n.t("#{I18N_PREFIX}not_supported")}"
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def human_readable_value(input)
|
|
62
|
+
current_clause = get_clause_by_id(input[:clause])
|
|
63
|
+
case input[:clause]
|
|
64
|
+
when *[CLAUSE_EQUALS, CLAUSE_DOESNT_EQUAL, CLAUSE_GREATER_THAN, CLAUSE_GREATER_THAN_OR_EQUAL, CLAUSE_LESS_THAN, CLAUSE_LESS_THAN_OR_EQUAL]
|
|
65
|
+
input[:value1]
|
|
66
|
+
when *[CLAUSE_BETWEEN, CLAUSE_NOT_BETWEEN]
|
|
67
|
+
"#{input[:value1]} #{I18n.t("#{I18N_PREFIX}and")} #{input[:value2]}"
|
|
68
|
+
when *[CLAUSE_SET, CLAUSE_NOT_SET]
|
|
69
|
+
""
|
|
70
|
+
else
|
|
71
|
+
raise "#{input[:clause]} #{I18n.t("#{I18N_PREFIX}not_supported")}"
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def clauses
|
|
78
|
+
[
|
|
79
|
+
Clause.new(CLAUSE_EQUALS, I18n.t("#{I18N_PREFIX}is")).requires_inputs(["value1"]),
|
|
80
|
+
|
|
81
|
+
Clause.new(CLAUSE_DOESNT_EQUAL, I18n.t("#{I18N_PREFIX}is_not")).requires_inputs(["value1"]),
|
|
82
|
+
|
|
83
|
+
Clause.new(CLAUSE_GREATER_THAN, I18n.t("#{I18N_PREFIX}is_gt")).requires_inputs(["value1"]),
|
|
84
|
+
|
|
85
|
+
Clause.new(CLAUSE_GREATER_THAN_OR_EQUAL, I18n.t("#{I18N_PREFIX}is_gtteq")).requires_inputs(["value1"]),
|
|
86
|
+
|
|
87
|
+
Clause.new(CLAUSE_LESS_THAN, I18n.t("#{I18N_PREFIX}is_lt")).requires_inputs(["value1"]),
|
|
88
|
+
|
|
89
|
+
Clause.new(CLAUSE_LESS_THAN_OR_EQUAL, I18n.t("#{I18N_PREFIX}is_lteq")).requires_inputs(["value1"]),
|
|
90
|
+
|
|
91
|
+
Clause.new(CLAUSE_BETWEEN, I18n.t("#{I18N_PREFIX}is_between")).requires_inputs(["value1", "value2"]),
|
|
92
|
+
|
|
93
|
+
Clause.new(CLAUSE_NOT_BETWEEN, I18n.t("#{I18N_PREFIX}is_not_between")).requires_inputs(["value1", "value2"]),
|
|
94
|
+
|
|
95
|
+
Clause.new(CLAUSE_SET, I18n.t("#{I18N_PREFIX}is_set")),
|
|
96
|
+
|
|
97
|
+
Clause.new(CLAUSE_NOT_SET, I18n.t("#{I18N_PREFIX}is_not_set")),
|
|
98
|
+
]
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def allow_floats
|
|
102
|
+
@floats = true
|
|
103
|
+
self
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def floats_not_allowed?
|
|
107
|
+
!@floats
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# TODO Refactor to remove input here
|
|
111
|
+
def apply_condition(input, table, _inverse_clause)
|
|
112
|
+
# TODO check for custom clause
|
|
113
|
+
|
|
114
|
+
case clause
|
|
115
|
+
when CLAUSE_EQUALS
|
|
116
|
+
apply_clause_equals(table, value1)
|
|
117
|
+
|
|
118
|
+
when CLAUSE_DOESNT_EQUAL
|
|
119
|
+
apply_clause_doesnt_equal(table, value1)
|
|
120
|
+
|
|
121
|
+
when CLAUSE_GREATER_THAN
|
|
122
|
+
apply_clause_greater_than(table, value1)
|
|
123
|
+
|
|
124
|
+
when CLAUSE_GREATER_THAN_OR_EQUAL
|
|
125
|
+
apply_clause_greater_than_or_equal(table, value1)
|
|
126
|
+
|
|
127
|
+
when CLAUSE_LESS_THAN
|
|
128
|
+
apply_clause_less_than(table, value1)
|
|
129
|
+
|
|
130
|
+
when CLAUSE_LESS_THAN_OR_EQUAL
|
|
131
|
+
apply_clause_less_than_or_equal(table, value1)
|
|
132
|
+
|
|
133
|
+
when CLAUSE_BETWEEN
|
|
134
|
+
apply_clause_between(table, value1, value2)
|
|
135
|
+
|
|
136
|
+
when CLAUSE_NOT_BETWEEN
|
|
137
|
+
apply_clause_not_between(table, value1, value2)
|
|
138
|
+
|
|
139
|
+
when CLAUSE_SET
|
|
140
|
+
apply_clause_set(table)
|
|
141
|
+
|
|
142
|
+
when CLAUSE_NOT_SET
|
|
143
|
+
apply_clause_not_set(table)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def input_could_include_zero?(input)
|
|
148
|
+
clause = input[:clause]
|
|
149
|
+
value1 = input[:value1].to_i
|
|
150
|
+
value2 = input[:value2].to_i
|
|
151
|
+
case clause
|
|
152
|
+
when CLAUSE_EQUALS
|
|
153
|
+
return value1 == 0
|
|
154
|
+
|
|
155
|
+
when CLAUSE_DOESNT_EQUAL
|
|
156
|
+
return value1 != 0
|
|
157
|
+
|
|
158
|
+
when CLAUSE_LESS_THAN_OR_EQUAL
|
|
159
|
+
return value1 >= 0
|
|
160
|
+
|
|
161
|
+
when CLAUSE_LESS_THAN
|
|
162
|
+
return value1 > 0
|
|
163
|
+
|
|
164
|
+
when CLAUSE_GREATER_THAN
|
|
165
|
+
return value1 < 0
|
|
166
|
+
|
|
167
|
+
when CLAUSE_GREATER_THAN_OR_EQUAL
|
|
168
|
+
return value1 <= 0
|
|
169
|
+
|
|
170
|
+
when CLAUSE_BETWEEN
|
|
171
|
+
return value1 <= 0 && value2 >= 0
|
|
172
|
+
|
|
173
|
+
when CLAUSE_NOT_BETWEEN
|
|
174
|
+
return (value1 > 0 && value2 > 0) || (value1 < 0 && value2 < 0)
|
|
175
|
+
|
|
176
|
+
when CLAUSE_SET
|
|
177
|
+
return false
|
|
178
|
+
|
|
179
|
+
when CLAUSE_NOT_SET
|
|
180
|
+
return false
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def apply_clause_equals(table, value)
|
|
185
|
+
table.grouping(arel_attribute(table).eq(value))
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def apply_clause_doesnt_equal(table, value)
|
|
189
|
+
table.grouping(arel_attribute(table).not_eq(value).or(arel_attribute(table).eq(nil)))
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def apply_clause_greater_than(table, value)
|
|
193
|
+
table.grouping(arel_attribute(table).gt(value))
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def apply_clause_greater_than_or_equal(table, value)
|
|
197
|
+
table.grouping(arel_attribute(table).gteq(value))
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def apply_clause_less_than(table, value)
|
|
201
|
+
table.grouping(arel_attribute(table).lt(value))
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def apply_clause_less_than_or_equal(table, value)
|
|
205
|
+
table.grouping(arel_attribute(table).lteq(value))
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def apply_clause_between(table, value1, value2)
|
|
209
|
+
table.grouping(arel_attribute(table).between(value1..value2))
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def apply_clause_not_between(table, value1, value2)
|
|
213
|
+
table.grouping(arel_attribute(table).not_between(value1..value2))
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def apply_clause_set(table)
|
|
217
|
+
table.grouping(arel_attribute(table).not_eq(nil))
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def apply_clause_not_set(table)
|
|
221
|
+
table.grouping(arel_attribute(table).eq(nil))
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
module Refine::Conditions
|
|
2
|
+
class OptionCondition < Condition
|
|
3
|
+
include HasClauses
|
|
4
|
+
include UsesAttributes
|
|
5
|
+
include ActiveModel::Validations
|
|
6
|
+
|
|
7
|
+
validate :select_is_array
|
|
8
|
+
validate :option_in_approved_list?
|
|
9
|
+
|
|
10
|
+
attr_reader :selected, :nil_option_id, :options
|
|
11
|
+
|
|
12
|
+
CLAUSE_EQUALS = Clauses::EQUALS
|
|
13
|
+
CLAUSE_DOESNT_EQUAL = Clauses::DOESNT_EQUAL
|
|
14
|
+
|
|
15
|
+
CLAUSE_IN = Clauses::IN
|
|
16
|
+
CLAUSE_NOT_IN = Clauses::NOT_IN
|
|
17
|
+
|
|
18
|
+
CLAUSE_SET = Clauses::SET
|
|
19
|
+
CLAUSE_NOT_SET = Clauses::NOT_SET
|
|
20
|
+
|
|
21
|
+
I18N_PREFIX = "refine.refine_blueprints.option_condition."
|
|
22
|
+
|
|
23
|
+
def component
|
|
24
|
+
"option-condition"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def human_readable(input)
|
|
28
|
+
current_clause = get_clause_by_id(input[:clause])
|
|
29
|
+
display_values = input[:selected]&.map {|option_id| get_options.call.find{|option| option[:id] == option_id}[:display]}.to_a
|
|
30
|
+
case input[:clause]
|
|
31
|
+
when *[CLAUSE_EQUALS, CLAUSE_DOESNT_EQUAL]
|
|
32
|
+
"#{display} #{current_clause.display} #{display_values.first}"
|
|
33
|
+
when *[CLAUSE_IN, CLAUSE_NOT_IN]
|
|
34
|
+
if display_values.length >= 3
|
|
35
|
+
display_values = display_values.take(2) + ["..."]
|
|
36
|
+
end
|
|
37
|
+
"#{display} #{current_clause.display}: #{display_values.join(", ")}"
|
|
38
|
+
when *[CLAUSE_SET, CLAUSE_NOT_SET]
|
|
39
|
+
"#{display} #{current_clause.display}"
|
|
40
|
+
else
|
|
41
|
+
raise "#{input[:clause]} #{I18n.t("#{I18N_PREFIX}not_supported")}"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def human_readable_value(input)
|
|
46
|
+
current_clause = get_clause_by_id(input[:clause])
|
|
47
|
+
display_values = input[:selected]&.map {|option_id| get_options.call.find{|option| option[:id] == option_id}[:display]}.to_a
|
|
48
|
+
case input[:clause]
|
|
49
|
+
when *[CLAUSE_EQUALS, CLAUSE_DOESNT_EQUAL]
|
|
50
|
+
display_values.first
|
|
51
|
+
when *[CLAUSE_IN, CLAUSE_NOT_IN]
|
|
52
|
+
if display_values.length >= 3
|
|
53
|
+
display_values = display_values.take(2) + ["..."]
|
|
54
|
+
end
|
|
55
|
+
display_values.join(", ")
|
|
56
|
+
when *[CLAUSE_SET, CLAUSE_NOT_SET]
|
|
57
|
+
""
|
|
58
|
+
else
|
|
59
|
+
raise "#{input[:clause]} #{I18n.t("#{I18N_PREFIX}not_supported")}"
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def boot
|
|
64
|
+
@nil_option_id = nil
|
|
65
|
+
@options = nil
|
|
66
|
+
# TODO @validate_selections = true
|
|
67
|
+
with_meta({options: get_options})
|
|
68
|
+
add_ensurance(ensure_options)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def set_input_parameters(input)
|
|
72
|
+
@selected = input[:selected]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def select_is_array
|
|
76
|
+
errors.add(:base, I18n.t("#{I18N_PREFIX}must_be_array")) unless selected.is_a?(Array)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def option_in_approved_list?
|
|
80
|
+
# TODO allow this to accept integers as well as strings. Right now must be a string.
|
|
81
|
+
return if selected.nil?
|
|
82
|
+
selected.each do |select|
|
|
83
|
+
select.join if select.is_a? Array
|
|
84
|
+
unless get_options.call.map { |option| option[:id] }.include? select
|
|
85
|
+
errors.add(:base, I18n.t("#{I18N_PREFIX}not_approved", select: select))
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def get_options
|
|
91
|
+
proc do
|
|
92
|
+
@options = Refine::Rails.configuration.option_condition_ordering.call(
|
|
93
|
+
call_proc_if_callable(options)
|
|
94
|
+
)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def ensure_options
|
|
99
|
+
proc do
|
|
100
|
+
developer_options = get_options.call
|
|
101
|
+
# Options must be sent in as an array
|
|
102
|
+
if !developer_options.is_a? Array
|
|
103
|
+
raise I18n.t("#{I18N_PREFIX}not_determined")
|
|
104
|
+
end
|
|
105
|
+
# Each option must be a hash of values that includes :id and :display
|
|
106
|
+
developer_options.each do |option|
|
|
107
|
+
if (!option.is_a? Hash) || option.keys.exclude?(:id) || option.keys.exclude?(:display)
|
|
108
|
+
raise Refine::Conditions::Errors::OptionError.new(I18n.t("#{I18N_PREFIX}must_have_id_and_display"))
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
ensure_no_duplicates(developer_options)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def ensure_no_duplicates(developer_options)
|
|
116
|
+
id_array = developer_options.map { |option| option[:id] }
|
|
117
|
+
duplicates = id_array.select { |id| id_array.count(id) > 1 }.uniq
|
|
118
|
+
if duplicates.present?
|
|
119
|
+
raise Refine::Conditions::Errors::OptionError.new(I18n.t("#{I18N_PREFIX}must_be_unique", duplicates: duplicates))
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def clauses
|
|
124
|
+
[
|
|
125
|
+
Clause.new(CLAUSE_EQUALS, I18n.t("#{I18N_PREFIX}is"))
|
|
126
|
+
.requires_inputs(["selected"])
|
|
127
|
+
.with_meta({multiple: false}),
|
|
128
|
+
|
|
129
|
+
Clause.new(CLAUSE_DOESNT_EQUAL, I18n.t("#{I18N_PREFIX}is_not"))
|
|
130
|
+
.requires_inputs(["selected"])
|
|
131
|
+
.with_meta({multiple: false}),
|
|
132
|
+
|
|
133
|
+
Clause.new(CLAUSE_IN, I18n.t("#{I18N_PREFIX}is_one_of"))
|
|
134
|
+
.requires_inputs(["selected"])
|
|
135
|
+
.with_meta({multiple: true}),
|
|
136
|
+
|
|
137
|
+
Clause.new(CLAUSE_NOT_IN, I18n.t("#{I18N_PREFIX}is_not_one_of"))
|
|
138
|
+
.requires_inputs(["selected"])
|
|
139
|
+
.with_meta({multiple: true}),
|
|
140
|
+
|
|
141
|
+
Clause.new(CLAUSE_SET, I18n.t("#{I18N_PREFIX}is_set")),
|
|
142
|
+
|
|
143
|
+
Clause.new(CLAUSE_NOT_SET, I18n.t("#{I18N_PREFIX}is_not_set"))
|
|
144
|
+
]
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def apply_condition(input, table, inverse_clause)
|
|
148
|
+
value = input[:selected]
|
|
149
|
+
# TODO: Triggers on "through" relationship. Other relationships?
|
|
150
|
+
@clause = CLAUSE_IN if inverse_clause
|
|
151
|
+
|
|
152
|
+
case clause
|
|
153
|
+
when CLAUSE_SET
|
|
154
|
+
apply_clause_set(table)
|
|
155
|
+
|
|
156
|
+
when CLAUSE_NOT_SET
|
|
157
|
+
apply_clause_not_set(table)
|
|
158
|
+
|
|
159
|
+
when CLAUSE_EQUALS
|
|
160
|
+
apply_clause_equals(value, table)
|
|
161
|
+
|
|
162
|
+
when CLAUSE_DOESNT_EQUAL
|
|
163
|
+
apply_clause_doesnt_equal(value, table)
|
|
164
|
+
|
|
165
|
+
when CLAUSE_IN
|
|
166
|
+
apply_clause_in(value, table)
|
|
167
|
+
|
|
168
|
+
when CLAUSE_NOT_IN
|
|
169
|
+
apply_clause_not_in(value, table)
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def with_options(developer_configured_options)
|
|
174
|
+
@options = developer_configured_options
|
|
175
|
+
self
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def with_nil_option(id)
|
|
179
|
+
@nil_option_id = id
|
|
180
|
+
self
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def nil_option_selected?(value)
|
|
184
|
+
# Return false if no nil option id
|
|
185
|
+
return false unless nil_option_id
|
|
186
|
+
value&.include? nil_option_id
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def values_for_application(ids, single = false)
|
|
190
|
+
# Get developer configured options with nil_option_id removed and select only elements from requested ids
|
|
191
|
+
# Extract values from either _value key or id key. _value can be a callable
|
|
192
|
+
values = get_options.call.delete_if { |el| el[:id] == nil_option_id }
|
|
193
|
+
.select { |value| ids.include? value[:id] }
|
|
194
|
+
.map! { |value| (value.has_key? :_value) ? call_proc_if_callable(value[:_value]) : value[:id] }
|
|
195
|
+
single ? values[0] : values
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def apply_nil_query(value, table)
|
|
199
|
+
table.grouping(table[:"#{attribute}"].eq(nil))
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def apply_not_nil_query(value, table)
|
|
203
|
+
table.grouping(table[:"#{attribute}"].not_eq(nil))
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def apply_equals(value, table)
|
|
207
|
+
table.grouping(table[:"#{attribute}"].eq(value))
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def apply_clause_in(value, table)
|
|
211
|
+
normalized_values = values_for_application(value)
|
|
212
|
+
|
|
213
|
+
if nil_option_selected?(value)
|
|
214
|
+
table.grouping(table[:"#{attribute}"].in(normalized_values).or(table[:"#{attribute}"].eq(nil)))
|
|
215
|
+
else
|
|
216
|
+
table.grouping(table[:"#{attribute}"].in(normalized_values))
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def apply_clause_not_in(value, table)
|
|
221
|
+
normalized_values = values_for_application(value)
|
|
222
|
+
# Must check for only nil option selected here
|
|
223
|
+
if nil_option_selected?(value) && value.one?
|
|
224
|
+
table.grouping(table[:"#{attribute}"].not_eq(nil))
|
|
225
|
+
elsif nil_option_selected?(value)
|
|
226
|
+
table.grouping(table[:"#{attribute}"].not_in(normalized_values).or(table[:"#{attribute}"].not_eq(nil)))
|
|
227
|
+
else
|
|
228
|
+
table.grouping(table[:"#{attribute}"].not_in(normalized_values).or(table[:"#{attribute}"].eq(nil)))
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def apply_clause_equals(value, table)
|
|
233
|
+
if nil_option_selected?(value)
|
|
234
|
+
apply_nil_query(value, table)
|
|
235
|
+
else
|
|
236
|
+
apply_equals(values_for_application(value, true), table)
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def apply_clause_doesnt_equal(value, table)
|
|
241
|
+
if nil_option_selected?(value)
|
|
242
|
+
apply_not_nil_query(value, table)
|
|
243
|
+
else
|
|
244
|
+
apply_not_equals(values_for_application(value, true), table)
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def apply_not_equals(value, table)
|
|
249
|
+
table.grouping(table[:"#{attribute}"].not_eq(value).or(table[:"#{attribute}"].eq(nil)))
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def apply_clause_set(table)
|
|
253
|
+
table.grouping(table[:"#{attribute}"].not_eq_any([nil, ""]))
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def apply_clause_not_set(table)
|
|
257
|
+
table.grouping(table[:"#{attribute}"].eq_any([nil, ""]))
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
end
|