refine-rails 2.9.0
Sign up to get free protection for your applications and to get access to all the features.
- 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,152 @@
|
|
1
|
+
module Refine::Conditions
|
2
|
+
class TextCondition < Condition
|
3
|
+
include HasClauses
|
4
|
+
|
5
|
+
CLAUSE_EQUALS = Clauses::EQUALS
|
6
|
+
CLAUSE_DOESNT_EQUAL = Clauses::DOESNT_EQUAL
|
7
|
+
|
8
|
+
CLAUSE_STARTS_WITH = Clauses::STARTS_WITH
|
9
|
+
CLAUSE_ENDS_WITH = Clauses::ENDS_WITH
|
10
|
+
CLAUSE_DOESNT_START_WITH = Clauses::DOESNT_START_WITH
|
11
|
+
CLAUSE_DOESNT_END_WITH = Clauses::DOESNT_END_WITH
|
12
|
+
|
13
|
+
CLAUSE_CONTAINS = Clauses::CONTAINS
|
14
|
+
CLAUSE_DOESNT_CONTAIN = Clauses::DOESNT_CONTAIN
|
15
|
+
|
16
|
+
CLAUSE_SET = Clauses::SET
|
17
|
+
|
18
|
+
CLAUSE_NOT_SET = Clauses::NOT_SET
|
19
|
+
|
20
|
+
I18N_PREFIX = "refine.refine_blueprints.text_condition."
|
21
|
+
|
22
|
+
def component
|
23
|
+
"text-condition"
|
24
|
+
end
|
25
|
+
|
26
|
+
def human_readable(input)
|
27
|
+
current_clause = get_clause_by_id(input[:clause])
|
28
|
+
if input[:clause].in? [CLAUSE_SET, CLAUSE_NOT_SET]
|
29
|
+
"#{display} #{current_clause.display}"
|
30
|
+
else
|
31
|
+
"#{display} #{current_clause.display} #{input[:value]}"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def human_readable_value(input)
|
36
|
+
current_clause = get_clause_by_id(input[:clause])
|
37
|
+
if input[:clause].in? [CLAUSE_SET, CLAUSE_NOT_SET]
|
38
|
+
""
|
39
|
+
else
|
40
|
+
input[:value]
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def clauses
|
45
|
+
[
|
46
|
+
Clause.new(CLAUSE_EQUALS, I18n.t("#{I18N_PREFIX}is"))
|
47
|
+
.requires_inputs(["value"]),
|
48
|
+
|
49
|
+
Clause.new(CLAUSE_DOESNT_EQUAL, I18n.t("#{I18N_PREFIX}is_not"))
|
50
|
+
.requires_inputs(["value"]),
|
51
|
+
|
52
|
+
Clause.new(CLAUSE_STARTS_WITH, I18n.t("#{I18N_PREFIX}starts_with"))
|
53
|
+
.requires_inputs(["value"]),
|
54
|
+
|
55
|
+
Clause.new(CLAUSE_ENDS_WITH, I18n.t("#{I18N_PREFIX}ends_with"))
|
56
|
+
.requires_inputs(["value"]),
|
57
|
+
|
58
|
+
Clause.new(CLAUSE_DOESNT_START_WITH, I18n.t("#{I18N_PREFIX}does_not_start_with"))
|
59
|
+
.requires_inputs(["value"]),
|
60
|
+
|
61
|
+
Clause.new(CLAUSE_DOESNT_END_WITH, I18n.t("#{I18N_PREFIX}does_not_end_with"))
|
62
|
+
.requires_inputs(["value"]),
|
63
|
+
|
64
|
+
Clause.new(CLAUSE_CONTAINS, I18n.t("#{I18N_PREFIX}contains"))
|
65
|
+
.requires_inputs(["value"]),
|
66
|
+
|
67
|
+
Clause.new(CLAUSE_DOESNT_CONTAIN, I18n.t("#{I18N_PREFIX}does_not_contain"))
|
68
|
+
.requires_inputs(["value"]),
|
69
|
+
|
70
|
+
Clause.new(CLAUSE_SET, I18n.t("#{I18N_PREFIX}is_set")),
|
71
|
+
|
72
|
+
Clause.new(CLAUSE_NOT_SET, I18n.t("#{I18N_PREFIX}is_not_set"))
|
73
|
+
]
|
74
|
+
end
|
75
|
+
|
76
|
+
def apply_condition(input, table, _inverse_clause)
|
77
|
+
value = input[:value]
|
78
|
+
|
79
|
+
case clause
|
80
|
+
when CLAUSE_EQUALS
|
81
|
+
apply_clause_equals(value, table)
|
82
|
+
|
83
|
+
when CLAUSE_DOESNT_EQUAL
|
84
|
+
apply_clause_doesnt_equal(value, table)
|
85
|
+
|
86
|
+
when CLAUSE_STARTS_WITH
|
87
|
+
apply_clause_starts_with(value, table)
|
88
|
+
|
89
|
+
when CLAUSE_ENDS_WITH
|
90
|
+
apply_clause_ends_with(value, table)
|
91
|
+
|
92
|
+
when CLAUSE_DOESNT_START_WITH
|
93
|
+
apply_clause_doesnt_start_with(value, table)
|
94
|
+
|
95
|
+
when CLAUSE_DOESNT_END_WITH
|
96
|
+
apply_clause_doesnt_end_with(value, table)
|
97
|
+
|
98
|
+
when CLAUSE_CONTAINS
|
99
|
+
apply_clause_contains(value, table)
|
100
|
+
|
101
|
+
when CLAUSE_DOESNT_CONTAIN
|
102
|
+
apply_clause_doesnt_contain(value, table)
|
103
|
+
|
104
|
+
when CLAUSE_SET
|
105
|
+
apply_clause_set(value, table)
|
106
|
+
|
107
|
+
when CLAUSE_NOT_SET
|
108
|
+
apply_clause_not_set(value, table)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def apply_clause_equals(value, table)
|
113
|
+
table.grouping(arel_attribute(table).eq(value))
|
114
|
+
end
|
115
|
+
|
116
|
+
def apply_clause_doesnt_equal(value, table)
|
117
|
+
table.grouping(arel_attribute(table).not_eq(value).or(arel_attribute(table).eq(nil)))
|
118
|
+
end
|
119
|
+
|
120
|
+
def apply_clause_starts_with(value, table)
|
121
|
+
table.grouping(arel_attribute(table).matches("#{value}%"))
|
122
|
+
end
|
123
|
+
|
124
|
+
def apply_clause_ends_with(value, table)
|
125
|
+
table.grouping(arel_attribute(table).matches("%#{value}"))
|
126
|
+
end
|
127
|
+
|
128
|
+
def apply_clause_contains(value, table)
|
129
|
+
table.grouping(arel_attribute(table).matches("%#{value}%"))
|
130
|
+
end
|
131
|
+
|
132
|
+
def apply_clause_doesnt_contain(value, table)
|
133
|
+
table.grouping(arel_attribute(table).does_not_match("%#{value}%").or(arel_attribute(table).eq(nil)))
|
134
|
+
end
|
135
|
+
|
136
|
+
def apply_clause_set(_, table)
|
137
|
+
table.grouping(arel_attribute(table).not_eq_all([nil, ""]))
|
138
|
+
end
|
139
|
+
|
140
|
+
def apply_clause_not_set(_, table)
|
141
|
+
table.grouping(arel_attribute(table).eq_any([nil, ""]))
|
142
|
+
end
|
143
|
+
|
144
|
+
def apply_clause_doesnt_start_with(value, table)
|
145
|
+
table.grouping(arel_attribute(table).does_not_match("#{value}%"))
|
146
|
+
end
|
147
|
+
|
148
|
+
def apply_clause_doesnt_end_with(value, table)
|
149
|
+
table.grouping(arel_attribute(table).does_not_match("%#{value}"))
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
@@ -0,0 +1,168 @@
|
|
1
|
+
module Refine::Conditions
|
2
|
+
module UsesAttributes
|
3
|
+
|
4
|
+
def with_attribute(value)
|
5
|
+
@attribute = value
|
6
|
+
self
|
7
|
+
end
|
8
|
+
|
9
|
+
def apply_relationship_attribute(input:, query:)
|
10
|
+
# Split on first .
|
11
|
+
decompose_attribute = @attribute.split(".", 2)
|
12
|
+
# Attribute now is the back half of the initial attribute
|
13
|
+
@attribute = decompose_attribute[1]
|
14
|
+
|
15
|
+
if !@attribute.include?(".")
|
16
|
+
# No more .s, deepest level
|
17
|
+
@on_deepest_relationship = true
|
18
|
+
end
|
19
|
+
# Relation to be handled
|
20
|
+
relation = decompose_attribute[0]
|
21
|
+
|
22
|
+
# Get the Reflection object which defines the relationship between query and relation
|
23
|
+
# First iteration pull relationship using base query which responds to model.
|
24
|
+
instance = if query.respond_to? :model
|
25
|
+
query.model.reflect_on_association(relation.to_sym)
|
26
|
+
else
|
27
|
+
# When query is sent in as subquery (recursive) the query object is the model class pulled from the
|
28
|
+
# previous instance value
|
29
|
+
query.reflect_on_association(relation.to_sym)
|
30
|
+
end
|
31
|
+
|
32
|
+
unless instance
|
33
|
+
raise "Relationship does not exist for #{relation}."
|
34
|
+
end
|
35
|
+
|
36
|
+
filter.set_pending_relationship(relation, instance)
|
37
|
+
|
38
|
+
# If the current condition is a refinement (filter refinement) set collapsible to true
|
39
|
+
if is_refinement
|
40
|
+
filter.allow_pending_relationship_to_collapse
|
41
|
+
end
|
42
|
+
|
43
|
+
if can_use_where_in_relationship_subquery?(instance)
|
44
|
+
create_pending_wherein_subquery(input: input, relation: relation, instance: instance, query: query)
|
45
|
+
else
|
46
|
+
create_pending_has_many_through_subquery(input: input, relation: relation, instance: instance, query: query)
|
47
|
+
end
|
48
|
+
|
49
|
+
filter.release_pending_relationship
|
50
|
+
# We want the method to return nil for relationship attributes
|
51
|
+
# The purpose of this method is to populate pending relationship subqueries
|
52
|
+
nil
|
53
|
+
end
|
54
|
+
|
55
|
+
def key_1(instance)
|
56
|
+
# Foreign key on belongs to, primary key on HasMany
|
57
|
+
if instance.is_a? ActiveRecord::Reflection::BelongsToReflection
|
58
|
+
instance.foreign_key.to_sym
|
59
|
+
else
|
60
|
+
instance.active_record_primary_key.to_sym
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def key_2(instance)
|
65
|
+
if instance.is_a? ActiveRecord::Reflection::BelongsToReflection
|
66
|
+
instance.active_record_primary_key.to_sym
|
67
|
+
else
|
68
|
+
instance.foreign_key.to_sym
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def create_pending_wherein_subquery(input:, relation:, instance:, query:)
|
73
|
+
# This method builds out the linking keys between the provided query model and the relation
|
74
|
+
# and saves it to pending relationship subqueries
|
75
|
+
# Class of the relation as held in the AR::Relation object
|
76
|
+
relation_class = instance.klass
|
77
|
+
|
78
|
+
# Pull what's already in the tracker at this depth if already traversed
|
79
|
+
subquery = filter.get_pending_relationship_subquery || relation_class.select([key_2(instance)]).arel
|
80
|
+
|
81
|
+
# Primary/secondary keys keep track of how to link tables
|
82
|
+
# If depth has been added (i.e. filter.pending_relationship_subquery_depth = [:btt_user, :btt_notes])
|
83
|
+
# This will add the [:children] key to the pending_relationship_subqueries tracker under the parent key [:btt_user]
|
84
|
+
filter.add_pending_relationship_subquery(subquery: subquery, primary_key: key_1(instance), secondary_key: key_2(instance))
|
85
|
+
|
86
|
+
# Apply the condition. If a nested relationship, this apply is adding the children key (with values) to the pending_relationship_subqueries tracker
|
87
|
+
# due to the recursive nature of the apply method. This is critical because it get is then "rolled up" in release_pending_relationship
|
88
|
+
node = apply(input, relation_class.arel_table, relation_class, false)
|
89
|
+
|
90
|
+
# If node is an AREL::SELECT manager we are allowing the apply condition to return a fully formed subquery - we replace
|
91
|
+
# the linking keys in the tracker with the fully formed select query
|
92
|
+
# Has not been tested more than one level deep
|
93
|
+
if node.is_a? Arel::SelectManager
|
94
|
+
filter.add_pending_relationship_subquery(subquery: node, primary_key: key_1(instance), secondary_key: key_2(instance))
|
95
|
+
elsif node
|
96
|
+
# This modifies subquery *in* the pending_relationship_subqueries tracker.
|
97
|
+
subquery.where(node)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def group(nodes)
|
102
|
+
Arel::Nodes::Grouping.new(nodes)
|
103
|
+
end
|
104
|
+
|
105
|
+
# Determine if the clause should be flipped. For example, "not_eq" => "eq". Must also change "in" to "not in" upstream
|
106
|
+
# @param [Object] instance
|
107
|
+
# @param [String] clause The join clause (example: `eq` or `not_eq`)
|
108
|
+
# @return [Boolean]
|
109
|
+
def should_inverse_clause?(instance, clause)
|
110
|
+
is_through_reflection = instance.is_a?(ActiveRecord::Reflection::ThroughReflection)
|
111
|
+
is_inverse_clause_flippable = Clauses::FLIPPABLE.include?(clause)
|
112
|
+
|
113
|
+
is_through_reflection && is_inverse_clause_flippable
|
114
|
+
end
|
115
|
+
|
116
|
+
def create_pending_has_many_through_subquery(input:, relation:, instance:, query:)
|
117
|
+
# In a has_many relationship the negative has to be flipped to positive.
|
118
|
+
inverse_clause = should_inverse_clause?(instance, input[:clause])
|
119
|
+
# Ex: A country has many posts through hmtt_users.
|
120
|
+
# Use AR to properly join the relation to the base query provided
|
121
|
+
# Convert to AREL to use with nodes
|
122
|
+
subquery_path = query.model.select(key_1(instance)).joins(relation.to_sym).arel
|
123
|
+
relation_table_being_queried = instance.klass.arel_table
|
124
|
+
|
125
|
+
relation_class = instance.klass
|
126
|
+
|
127
|
+
node_to_apply = apply(input, relation_table_being_queried, relation_class, inverse_clause)
|
128
|
+
|
129
|
+
complete_subquery = subquery_path.where(node_to_apply)
|
130
|
+
subquery = filter.get_pending_relationship_subquery || complete_subquery
|
131
|
+
filter.add_pending_relationship_subquery(subquery: subquery, primary_key: key_1(instance), secondary_key: nil, inverse_clause: inverse_clause)
|
132
|
+
end
|
133
|
+
|
134
|
+
def can_use_where_in_relationship_subquery?(instance)
|
135
|
+
# Where in only works for belongs to, has one, or has many
|
136
|
+
(instance.is_a? ActiveRecord::Reflection::BelongsToReflection) || (instance.is_a? ActiveRecord::Reflection::HasManyReflection) || (instance.is_a? ActiveRecord::Reflection::HasOneReflection)
|
137
|
+
end
|
138
|
+
|
139
|
+
def is_relationship_attribute?
|
140
|
+
# TODO: Allow user to decide attribute is not a relationship
|
141
|
+
# If we are on the deepest relationship, it's no longer a relationship attribute
|
142
|
+
return false if @on_deepest_relationship
|
143
|
+
# If the attribute includes a ., it's a relationship attribute
|
144
|
+
@attribute.include?(".")
|
145
|
+
end
|
146
|
+
|
147
|
+
def raw_attribute(attribute)
|
148
|
+
@attribute = Arel.sql(attribute)
|
149
|
+
|
150
|
+
self
|
151
|
+
end
|
152
|
+
|
153
|
+
# TODO Examine the existing relationships and suggest model names if not instance is found
|
154
|
+
# def get_relationships(query)
|
155
|
+
# if query.respond_to? :model
|
156
|
+
# associations = query.model.reflect_on_all_associations
|
157
|
+
# else
|
158
|
+
# associations = query.reflect_on_all_associations
|
159
|
+
# end
|
160
|
+
# associations.map{|entry| puts entry.class, entry.foreign_key, entry.klass }
|
161
|
+
# differences=[]
|
162
|
+
# associations.each do association
|
163
|
+
# differences << String::Similarity.levenshtein_distance(relation, association )
|
164
|
+
# end
|
165
|
+
# differences
|
166
|
+
# end
|
167
|
+
end
|
168
|
+
end
|
@@ -0,0 +1,302 @@
|
|
1
|
+
module Refine
|
2
|
+
class Filter
|
3
|
+
include ActiveModel::Validations
|
4
|
+
include ActiveModel::Callbacks
|
5
|
+
include TracksPendingRelationshipSubqueries
|
6
|
+
include Stabilize
|
7
|
+
# This validation structure sents `initial_query` as the method to validate against
|
8
|
+
define_model_callbacks :initialize, only: [:after]
|
9
|
+
after_initialize :valid?
|
10
|
+
|
11
|
+
cattr_accessor :default_stabilizer, default: nil, instance_accessor: false
|
12
|
+
cattr_accessor :criteria_limit, default: 5, instance_accessor: true
|
13
|
+
|
14
|
+
attr_reader :blueprint, :initial_query
|
15
|
+
|
16
|
+
# Give each Filter subclass its own default_condition_id,
|
17
|
+
# that is also also readable from instances
|
18
|
+
#
|
19
|
+
# class UserFilter
|
20
|
+
# self.default_condition_id = "email"
|
21
|
+
# # ...
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
class << self
|
25
|
+
attr_accessor :default_condition_id
|
26
|
+
end
|
27
|
+
delegate :default_condition_id, to: :class
|
28
|
+
|
29
|
+
def initialize(blueprint = nil, query_scope = nil)
|
30
|
+
run_callbacks :initialize do
|
31
|
+
# If using this in test mode, `blueprint` will be an instance of
|
32
|
+
# `Blueprint` and the value must be extracted
|
33
|
+
if blueprint.is_a? Blueprints::Blueprint
|
34
|
+
blueprint = blueprint.to_array
|
35
|
+
end
|
36
|
+
@initial_query = query_scope
|
37
|
+
@blueprint = blueprint
|
38
|
+
@relation = initial_query
|
39
|
+
@immediately_commit_pending_relationship_subqueries = false
|
40
|
+
@@default_stabilizer = Refine::Stabilizers::UrlEncodedStabilizer
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# DEPRECATED use Refine::Filters::Criterion#human_readable instead
|
45
|
+
def human_readable_criterions
|
46
|
+
output = []
|
47
|
+
if blueprint.present?
|
48
|
+
blueprint.each do |criterion|
|
49
|
+
if criterion[:type] == "conjunction"
|
50
|
+
output << criterion[:word]
|
51
|
+
else
|
52
|
+
output << get_condition_for_criterion(criterion).human_readable(criterion[:input])
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
output
|
57
|
+
end
|
58
|
+
|
59
|
+
def automatically_stabilize?
|
60
|
+
true
|
61
|
+
end
|
62
|
+
|
63
|
+
# e.g. ContactsFilter -> Contact
|
64
|
+
def model
|
65
|
+
initial_query&.model || self.class.to_s.gsub(/Filter$/, "").singularize.constantize
|
66
|
+
end
|
67
|
+
|
68
|
+
def table
|
69
|
+
model.arel_table
|
70
|
+
end
|
71
|
+
|
72
|
+
def valid_query?
|
73
|
+
get_query
|
74
|
+
errors.none?
|
75
|
+
end
|
76
|
+
|
77
|
+
def get_query
|
78
|
+
raise "Initial query must exist" if initial_query.nil?
|
79
|
+
if blueprint.present?
|
80
|
+
@relation.where(group(make_sub_query(blueprint)))
|
81
|
+
else
|
82
|
+
@relation
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def get_query!
|
87
|
+
result = get_query
|
88
|
+
raise Refine::InvalidFilterError.new(filter: self) unless errors.none?
|
89
|
+
result
|
90
|
+
end
|
91
|
+
|
92
|
+
def add_nodes_to_query(subquery:, nodes:, query_method:)
|
93
|
+
# Apply existing nodes to existing subquery
|
94
|
+
if subquery.present? && nodes.present?
|
95
|
+
subquery = if query_method == "and"
|
96
|
+
# Apply the nodes using the AREL AND method
|
97
|
+
subquery.send(query_method, group(nodes))
|
98
|
+
else
|
99
|
+
# Override the AREL OR method in order to remove the automatic parens
|
100
|
+
Arel::Nodes::Or.new(subquery, group(nodes))
|
101
|
+
end
|
102
|
+
# No nodes returned, do nothing
|
103
|
+
elsif subquery.present? && nodes.blank?
|
104
|
+
subquery
|
105
|
+
# Subquery has not yet been initialized, initialize with new nodes - must use !nil? here, present/exists/blank etc don't
|
106
|
+
# account for AR::Relation object.
|
107
|
+
elsif subquery.blank? && !nodes.nil?
|
108
|
+
subquery = group(nodes)
|
109
|
+
end
|
110
|
+
subquery
|
111
|
+
end
|
112
|
+
|
113
|
+
def make_sub_query(modified_blueprint, depth = 0, subquery = nil)
|
114
|
+
# Need index control to directly skip indicies in fast forward (recursive call)
|
115
|
+
index = 0
|
116
|
+
while index < modified_blueprint.length
|
117
|
+
criterion = modified_blueprint[index]
|
118
|
+
# Decreasing depth, pass control back to caller
|
119
|
+
break if criterion[:depth] < depth
|
120
|
+
|
121
|
+
# If it's a conjunction, the next condition will handle it.
|
122
|
+
if criterion[:type] == "conjunction"
|
123
|
+
index += 1
|
124
|
+
next
|
125
|
+
end
|
126
|
+
|
127
|
+
# Check the word on the previous blueprint method. If it is not 'and'....?
|
128
|
+
query_method = if index == 0
|
129
|
+
"and"
|
130
|
+
else
|
131
|
+
modified_blueprint[index - 1][:word] == "and" ? "and" : "or"
|
132
|
+
end
|
133
|
+
|
134
|
+
# If the new depth is deeper than our current depth, that means we're
|
135
|
+
# starting a new group. We'll recursively call this method again
|
136
|
+
# with a subset of the blueprint.
|
137
|
+
if criterion[:depth] > depth
|
138
|
+
# Modify the array to send in the elements not yet handled (depth>current depth)
|
139
|
+
new_depth_array = modified_blueprint[index..-1]
|
140
|
+
|
141
|
+
# Return the nodes in () for elements on the same depth
|
142
|
+
recursive_nodes = make_sub_query(new_depth_array, depth + 1)
|
143
|
+
|
144
|
+
# Add the recursive subquery nodes to the existing query and modify the query
|
145
|
+
subquery = add_nodes_to_query(subquery: subquery, nodes: recursive_nodes, query_method: query_method)
|
146
|
+
|
147
|
+
# Skip indexes handled by recursive call
|
148
|
+
index = fast_forward(index, modified_blueprint, depth) + 1
|
149
|
+
next
|
150
|
+
end
|
151
|
+
# If there are any ORs at this depth, commit subqueries
|
152
|
+
@immediately_commit_pending_relationship_subqueries = if modified_blueprint.select { |item| (item[:type] == "conjunction" && item[:word] == "or" && item[:depth] == depth) }.present?
|
153
|
+
true
|
154
|
+
else
|
155
|
+
false
|
156
|
+
end
|
157
|
+
|
158
|
+
# If it is a relationship attribute apply_condition will call apply_relationship_attribute which will set up the pending relationship
|
159
|
+
# subquery but will not return a value.
|
160
|
+
# apply condition is NOT idempotent, hence the placeholder var
|
161
|
+
nodes_to_apply = apply_condition(criterion)
|
162
|
+
# If an error has been added to the errors array from apply_condition, do not continue execution at this level
|
163
|
+
if errors.any?
|
164
|
+
index += 1
|
165
|
+
next
|
166
|
+
end
|
167
|
+
|
168
|
+
subquery = add_nodes_to_query(subquery: subquery, nodes: nodes_to_apply, query_method: query_method)
|
169
|
+
|
170
|
+
if @immediately_commit_pending_relationship_subqueries.present?
|
171
|
+
committed_nodes_from_pending = commit_pending_relationship_subqueries
|
172
|
+
subquery = add_nodes_to_query(subquery: subquery, nodes: committed_nodes_from_pending, query_method: query_method)
|
173
|
+
end
|
174
|
+
|
175
|
+
index += 1
|
176
|
+
end
|
177
|
+
|
178
|
+
unless errors.any?
|
179
|
+
final_depth_nodes = commit_pending_relationship_subqueries
|
180
|
+
# Add nodes to existing query and return existing query
|
181
|
+
add_nodes_to_query(subquery: subquery, nodes: final_depth_nodes, query_method: query_method)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
def fast_forward(index, modified_blueprint, depth)
|
186
|
+
fast_forward_index = (index..modified_blueprint.length - 1).each do |cursor|
|
187
|
+
break cursor if modified_blueprint[cursor][:depth] <= depth
|
188
|
+
end
|
189
|
+
# TODO refactor for clarity. If I break early from the iterator I have an int. If not,
|
190
|
+
# default to modfied_blueprint.length -1 as the correct value
|
191
|
+
(fast_forward_index.is_a? Integer) ? fast_forward_index : modified_blueprint.length - 1
|
192
|
+
end
|
193
|
+
|
194
|
+
def group(nodes)
|
195
|
+
Arel::Nodes::Grouping.new(nodes)
|
196
|
+
end
|
197
|
+
|
198
|
+
def apply_condition(criterion)
|
199
|
+
begin
|
200
|
+
get_condition_for_criterion(criterion)&.apply(criterion[:input], table, initial_query)
|
201
|
+
rescue Refine::Conditions::Errors::ConditionClauseError => e
|
202
|
+
e.errors.each do |error|
|
203
|
+
errors.add(:base, error.full_message, criterion_uid: criterion[:uid])
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
def get_condition_for_criterion(criterion)
|
209
|
+
# Returns the object that matches the condition. Adds errors if not found.
|
210
|
+
# This checks the id on the condition such as text_test
|
211
|
+
returned_object = conditions.find { |condition| condition.id == criterion[:condition_id] }
|
212
|
+
if returned_object.nil?
|
213
|
+
errors.add(:filter, "The condition ID #{criterion[:condition_id]} was not found")
|
214
|
+
else
|
215
|
+
# Set filter variable on condition
|
216
|
+
instantiate_condition(returned_object)
|
217
|
+
end
|
218
|
+
# Must duplicate the condition so nested attributes don't bleed into one another
|
219
|
+
returned_object.dup
|
220
|
+
end
|
221
|
+
|
222
|
+
def configuration
|
223
|
+
{
|
224
|
+
type: "Refine",
|
225
|
+
class_name: self.class.name,
|
226
|
+
blueprint: @blueprint,
|
227
|
+
conditions: conditions_to_array,
|
228
|
+
stable_id: to_optional_stable_id
|
229
|
+
}
|
230
|
+
end
|
231
|
+
|
232
|
+
def conditions_to_array
|
233
|
+
return nil unless conditions
|
234
|
+
# Set filter object on condition and return to_array
|
235
|
+
conditions.map { |condition| instantiate_condition(condition) }.map(&:to_array)
|
236
|
+
end
|
237
|
+
|
238
|
+
def instantiate_condition(condition)
|
239
|
+
condition.set_filter(self)
|
240
|
+
translate_display(condition)
|
241
|
+
condition
|
242
|
+
end
|
243
|
+
|
244
|
+
# Set filter object on condition and sort alphabetically
|
245
|
+
def instantiated_conditions
|
246
|
+
conditions
|
247
|
+
.map { |c| instantiate_condition(c.dup) }
|
248
|
+
.sort_by { |c| c.display.to_s.downcase }
|
249
|
+
end
|
250
|
+
|
251
|
+
def translate_display(condition)
|
252
|
+
# If there are no locale definitions for this condition's subject, we can allow I18n to use a human-readable version of the ID.
|
253
|
+
# But, ideally, they have locales defined and we can find one of those.
|
254
|
+
label_fallback = {default: condition.id.humanize(keep_id_suffix: true).titleize}
|
255
|
+
condition.display = condition.display || I18n.t(".filter.conditions.#{condition.id}.label", default: I18n.t(".fields.#{condition.id}.label", **label_fallback))
|
256
|
+
end
|
257
|
+
|
258
|
+
def state
|
259
|
+
{
|
260
|
+
type: type,
|
261
|
+
blueprint: blueprint
|
262
|
+
}.to_json
|
263
|
+
end
|
264
|
+
|
265
|
+
def type
|
266
|
+
self.class.name
|
267
|
+
end
|
268
|
+
|
269
|
+
def self.from_state(state, initial_query = nil)
|
270
|
+
klass = state[:type].constantize
|
271
|
+
filter = klass.new(state[:blueprint], initial_query)
|
272
|
+
end
|
273
|
+
|
274
|
+
def self.default_stable_id_generator(klass)
|
275
|
+
if klass.method_defined?(:to_stable_id) && klass.method_defined?(:from_stable_id)
|
276
|
+
@@default_stabilizer = klass
|
277
|
+
else
|
278
|
+
raise ArgumentError.new('Given class doesn\'t implement to_stable_id and from_stable_id!')
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
def to_stable_id
|
283
|
+
Refine::Rails.configuration.stabilizer_classes[:url].new.to_stable_id(filter: self)
|
284
|
+
end
|
285
|
+
|
286
|
+
def blueprint_criteria
|
287
|
+
blueprint&.filter { |node| node.is_a?(Hash) && node[:type] == 'criterion' }.to_a
|
288
|
+
end
|
289
|
+
|
290
|
+
def criteria_limit_exceeded?
|
291
|
+
criteria_limit_set? && blueprint_criteria.length > criteria_limit
|
292
|
+
end
|
293
|
+
|
294
|
+
def criteria_limit_reached?
|
295
|
+
criteria_limit_set? && blueprint_criteria.length >= criteria_limit
|
296
|
+
end
|
297
|
+
|
298
|
+
def criteria_limit_set?
|
299
|
+
criteria_limit.to_i.positive?
|
300
|
+
end
|
301
|
+
end
|
302
|
+
end
|