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,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
|