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,464 @@
|
|
1
|
+
module Refine::Conditions
|
2
|
+
class DateCondition < Condition
|
3
|
+
include ActiveModel::Validations
|
4
|
+
include HasClauses
|
5
|
+
|
6
|
+
validate :date1_must_be_real, :date2_must_be_real, :date1_must_be_less_than_date2
|
7
|
+
|
8
|
+
cattr_accessor :default_user_timezone, default: "UTC", instance_accessor: false
|
9
|
+
cattr_accessor :default_database_timezone, default: "UTC", instance_accessor: false
|
10
|
+
attr_reader :date1, :date2, :days, :show_human_readable_timezone
|
11
|
+
|
12
|
+
CLAUSE_EQUALS = Clauses::EQUALS
|
13
|
+
CLAUSE_DOESNT_EQUAL = Clauses::DOESNT_EQUAL
|
14
|
+
|
15
|
+
CLAUSE_LESS_THAN = Clauses::LESS_THAN
|
16
|
+
CLAUSE_LESS_THAN_OR_EQUAL = Clauses::LESS_THAN_OR_EQUAL
|
17
|
+
|
18
|
+
CLAUSE_GREATER_THAN = Clauses::GREATER_THAN
|
19
|
+
CLAUSE_GREATER_THAN_OR_EQUAL = Clauses::GREATER_THAN_OR_EQUAL
|
20
|
+
|
21
|
+
CLAUSE_BETWEEN = Clauses::BETWEEN
|
22
|
+
CLAUSE_NOT_BETWEEN = Clauses::NOT_BETWEEN
|
23
|
+
|
24
|
+
CLAUSE_EXACTLY = Clauses::EXACTLY
|
25
|
+
CLAUSE_SET = Clauses::SET
|
26
|
+
CLAUSE_NOT_SET = Clauses::NOT_SET
|
27
|
+
|
28
|
+
ATTRIBUTE_TYPE_DATE = 0
|
29
|
+
ATTRIBUTE_TYPE_DATE_WITH_TIME = 1
|
30
|
+
ATTRIBUTE_TYPE_UNIX_TIMESTAMP = 2
|
31
|
+
|
32
|
+
I18N_PREFIX = "refine.refine_blueprints.date_condition."
|
33
|
+
|
34
|
+
def date1_must_be_real
|
35
|
+
return true unless date1
|
36
|
+
begin
|
37
|
+
Date.strptime(date1, "%Y-%m-%d")
|
38
|
+
rescue ArgumentError
|
39
|
+
errors.add(:base, I18n.t("#{I18N_PREFIX}date1_error"))
|
40
|
+
false
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def date2_must_be_real
|
45
|
+
return true unless date2
|
46
|
+
begin
|
47
|
+
Date.strptime(date2, "%Y-%m-%d")
|
48
|
+
rescue ArgumentError
|
49
|
+
errors.add(:base, I18n.t("#{I18N_PREFIX}date2_error"))
|
50
|
+
false
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def date1_must_be_less_than_date2
|
55
|
+
return true unless date1 && date2
|
56
|
+
if Date.strptime(date1, "%Y-%m-%d") > Date.strptime(date2, "%Y-%m-%d")
|
57
|
+
errors.add(:base, I18n.t("#{I18N_PREFIX}date1_greater_date2_error"))
|
58
|
+
false
|
59
|
+
else
|
60
|
+
true
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def boot
|
65
|
+
@attribute_type = @attribute_type ||= ATTRIBUTE_TYPE_DATE
|
66
|
+
add_ensurance(ensure_timezone)
|
67
|
+
end
|
68
|
+
|
69
|
+
def set_input_parameters(input)
|
70
|
+
@date1 = input[:date1]
|
71
|
+
@date2 = input[:date2]
|
72
|
+
@days = input[:days]
|
73
|
+
end
|
74
|
+
|
75
|
+
def ensure_timezone
|
76
|
+
proc do
|
77
|
+
timezone_exists(database_timezone)
|
78
|
+
timezone_exists(user_timezone)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def timezone_exists(zone)
|
83
|
+
return if ActiveSupport::TimeZone[zone].present?
|
84
|
+
errors.add(:base, I18n.t("#{I18N_PREFIX}timezone_error", zone: zone))
|
85
|
+
end
|
86
|
+
|
87
|
+
def component
|
88
|
+
"date-condition"
|
89
|
+
end
|
90
|
+
|
91
|
+
# Returns the string representation of the timezone localized if you don't already have a Time object to work with
|
92
|
+
# NOTE: It is possible for a timezone to not have an acceptable abbreviation. In those cases the Time library outputs unhelpful shortened offsets.
|
93
|
+
# EG: "International Date Line West" outputs "-12"
|
94
|
+
# So parse the string to see if it's one of these shortened forms and if it is, restructure to a fully formed GMT offset EG "GMT-12:00"
|
95
|
+
def timezone_abbr
|
96
|
+
if @show_human_readable_timezone
|
97
|
+
tz_string = " (#{I18n.l(Time.now.in_time_zone(user_timezone), format: :z)})"
|
98
|
+
match = tz_string =~ /^ \([-+]?\d+\)$/
|
99
|
+
if !((tz_string =~ /^ \([-+]?\d+\)$/).nil?)
|
100
|
+
tz_string = get_standard_tz_offset(tz_string)
|
101
|
+
end
|
102
|
+
tz_string
|
103
|
+
else
|
104
|
+
""
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# We should probably consider using tzinfo or some other library for this in the future
|
109
|
+
def get_standard_tz_offset(offset_string)
|
110
|
+
stripped = offset_string.strip
|
111
|
+
matches = stripped.match /^\s*\(([+-]?)(\d{1,4})\)\s*$/
|
112
|
+
unless matches.length > 1
|
113
|
+
return offset_string
|
114
|
+
end
|
115
|
+
|
116
|
+
sign = matches[1]
|
117
|
+
digits = matches[2].ljust(4, '0')
|
118
|
+
gmt_offset = " (GMT#{sign}#{digits[0..1]}:#{digits[2..3]})"
|
119
|
+
|
120
|
+
gmt_offset
|
121
|
+
end
|
122
|
+
|
123
|
+
def human_readable(input)
|
124
|
+
current_clause = get_clause_by_id(input[:clause])
|
125
|
+
|
126
|
+
case input[:clause]
|
127
|
+
when *[CLAUSE_EQUALS, CLAUSE_DOESNT_EQUAL, CLAUSE_LESS_THAN_OR_EQUAL, CLAUSE_GREATER_THAN_OR_EQUAL]
|
128
|
+
formatted_date1 = I18n.l(input[:date1].to_date, format: :dmy)
|
129
|
+
"#{display} #{current_clause.display} #{formatted_date1}#{timezone_abbr}"
|
130
|
+
when *[CLAUSE_BETWEEN, CLAUSE_NOT_BETWEEN]
|
131
|
+
formatted_date1 = I18n.l(input[:date1].to_date, format: :dmy)
|
132
|
+
formatted_date2 = I18n.l(input[:date2].to_date, format: :dmy)
|
133
|
+
and_i18n = I18n.t("#{I18N_PREFIX}and")
|
134
|
+
|
135
|
+
if formatted_date1 == formatted_date2
|
136
|
+
"#{display} #{get_clause_by_id(CLAUSE_EQUALS).display} #{formatted_date1}#{timezone_abbr}"
|
137
|
+
else
|
138
|
+
"#{display} #{current_clause.display} #{formatted_date1} #{and_i18n} #{formatted_date2}#{timezone_abbr}"
|
139
|
+
end
|
140
|
+
when *[CLAUSE_GREATER_THAN, CLAUSE_LESS_THAN, CLAUSE_EXACTLY]
|
141
|
+
days_i18n = I18n.t("#{I18N_PREFIX}days")
|
142
|
+
ago_i18n = I18n.t("#{I18N_PREFIX}ago")
|
143
|
+
from_now_i18n = I18n.t("#{I18N_PREFIX}from_now")
|
144
|
+
"#{display} #{current_clause.display} #{input[:days]} #{days_i18n} #{input[:modifier] == 'ago' ? ago_i18n : from_now_i18n}#{timezone_abbr}"
|
145
|
+
when *[CLAUSE_SET, CLAUSE_NOT_SET]
|
146
|
+
"#{display} #{current_clause.display}"
|
147
|
+
else
|
148
|
+
not_supported_i18n = I18n.t("#{I18N_PREFIX}not_supported")
|
149
|
+
raise "#{input[:clause]} #{not_supported_i18n}"
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def human_readable_value(input)
|
154
|
+
current_clause = get_clause_by_id(input[:clause])
|
155
|
+
|
156
|
+
case input[:clause]
|
157
|
+
when *[CLAUSE_EQUALS, CLAUSE_DOESNT_EQUAL, CLAUSE_LESS_THAN_OR_EQUAL, CLAUSE_GREATER_THAN_OR_EQUAL]
|
158
|
+
formatted_date1 = I18n.l(input[:date1].to_date, format: :dmy)
|
159
|
+
"#{formatted_date1}#{timezone_abbr}"
|
160
|
+
when *[CLAUSE_BETWEEN, CLAUSE_NOT_BETWEEN]
|
161
|
+
formatted_date1 = I18n.l(input[:date1].to_date, format: :dmy)
|
162
|
+
formatted_date2 = I18n.l(input[:date2].to_date, format: :dmy)
|
163
|
+
and_i18n = I18n.t("#{I18N_PREFIX}and")
|
164
|
+
|
165
|
+
if formatted_date1 == formatted_date2
|
166
|
+
"#{formatted_date1}#{timezone_abbr}"
|
167
|
+
else
|
168
|
+
"#{formatted_date1} #{and_i18n} #{formatted_date2}#{timezone_abbr}"
|
169
|
+
end
|
170
|
+
when *[CLAUSE_GREATER_THAN, CLAUSE_LESS_THAN, CLAUSE_EXACTLY]
|
171
|
+
days_i18n = I18n.t("#{I18N_PREFIX}and")
|
172
|
+
ago_i18n = I18n.t("#{I18N_PREFIX}days")
|
173
|
+
from_now_i18n = I18n.t("#{I18N_PREFIX}ago")
|
174
|
+
"#{input[:days]} #{days_i18n} #{input[:modifier] == 'ago' ? ago_i18n : from_now_i18n}#{timezone_abbr}"
|
175
|
+
when *[CLAUSE_SET, CLAUSE_NOT_SET]
|
176
|
+
""
|
177
|
+
else
|
178
|
+
not_supported_i18n = I18n.t("#{I18N_PREFIX}not_supported")
|
179
|
+
raise "#{input[:clause]} #{not_supported_i18n}"
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
def attribute_is_date
|
184
|
+
attribute_is(ATTRIBUTE_TYPE_DATE)
|
185
|
+
self
|
186
|
+
end
|
187
|
+
|
188
|
+
def attribute_is_date_with_time
|
189
|
+
attribute_is(ATTRIBUTE_TYPE_DATE_WITH_TIME)
|
190
|
+
self
|
191
|
+
end
|
192
|
+
|
193
|
+
def attribute_is_unix_timestamp # time
|
194
|
+
attribute_is(ATTRIBUTE_TYPE_UNIX_TIMESTAMP)
|
195
|
+
end
|
196
|
+
|
197
|
+
def attribute_is(type)
|
198
|
+
@attribute_type = type
|
199
|
+
self
|
200
|
+
end
|
201
|
+
|
202
|
+
def with_human_readable_timezone(show)
|
203
|
+
@show_human_readable_timezone = show
|
204
|
+
self
|
205
|
+
end
|
206
|
+
|
207
|
+
def with_database_timezone(timezone)
|
208
|
+
@database_timezone = timezone
|
209
|
+
self
|
210
|
+
end
|
211
|
+
|
212
|
+
def with_user_timezone(timezone)
|
213
|
+
@user_timezone = timezone
|
214
|
+
self
|
215
|
+
end
|
216
|
+
|
217
|
+
def get_timezone(zone)
|
218
|
+
call_proc_if_callable(zone)
|
219
|
+
end
|
220
|
+
|
221
|
+
def user_timezone
|
222
|
+
get_timezone(@user_timezone ||= @@default_user_timezone)
|
223
|
+
end
|
224
|
+
|
225
|
+
def database_timezone
|
226
|
+
get_timezone(@database_timezone ||= @@default_database_timezone)
|
227
|
+
end
|
228
|
+
|
229
|
+
def clauses
|
230
|
+
[
|
231
|
+
Clause.new(CLAUSE_EQUALS, I18n.t("#{I18N_PREFIX}is_on"))
|
232
|
+
.requires_inputs("date1"),
|
233
|
+
|
234
|
+
Clause.new(CLAUSE_DOESNT_EQUAL, I18n.t("#{I18N_PREFIX}not_on"))
|
235
|
+
.requires_inputs("date1"),
|
236
|
+
|
237
|
+
Clause.new(CLAUSE_LESS_THAN_OR_EQUAL, I18n.t("#{I18N_PREFIX}is_on_or_before"))
|
238
|
+
.requires_inputs("date1"),
|
239
|
+
|
240
|
+
Clause.new(CLAUSE_GREATER_THAN_OR_EQUAL, I18n.t("#{I18N_PREFIX}is_on_or_after"))
|
241
|
+
.requires_inputs("date1"),
|
242
|
+
|
243
|
+
Clause.new(CLAUSE_BETWEEN, I18n.t("#{I18N_PREFIX}is_between"))
|
244
|
+
.requires_inputs(["date1", "date2"]),
|
245
|
+
|
246
|
+
Clause.new(CLAUSE_NOT_BETWEEN, I18n.t("#{I18N_PREFIX}is_not_between"))
|
247
|
+
.requires_inputs(["date1", "date2"]),
|
248
|
+
|
249
|
+
Clause.new(CLAUSE_GREATER_THAN, I18n.t("#{I18N_PREFIX}is_more_than"))
|
250
|
+
.requires_inputs(["days", "modifier"]),
|
251
|
+
|
252
|
+
Clause.new(CLAUSE_EXACTLY, I18n.t("#{I18N_PREFIX}is"))
|
253
|
+
.requires_inputs(["days", "modifier"]),
|
254
|
+
|
255
|
+
Clause.new(CLAUSE_LESS_THAN, I18n.t("#{I18N_PREFIX}is_less_than"))
|
256
|
+
.requires_inputs(["days", "modifier"]),
|
257
|
+
|
258
|
+
Clause.new(CLAUSE_SET, I18n.t("#{I18N_PREFIX}is_set")),
|
259
|
+
|
260
|
+
Clause.new(CLAUSE_NOT_SET, I18n.t("#{I18N_PREFIX}is_not_set")),
|
261
|
+
]
|
262
|
+
end
|
263
|
+
|
264
|
+
def relative_clauses
|
265
|
+
[CLAUSE_GREATER_THAN, CLAUSE_LESS_THAN, CLAUSE_EXACTLY]
|
266
|
+
end
|
267
|
+
|
268
|
+
def is_relative_clause?
|
269
|
+
relative_clauses.include? clause
|
270
|
+
end
|
271
|
+
|
272
|
+
def modify_date_and_clause!(input)
|
273
|
+
@date1 = comparison_date(input)
|
274
|
+
@clause = standardize_clause(input)
|
275
|
+
end
|
276
|
+
|
277
|
+
def apply_condition(input, table, _inverse_clause)
|
278
|
+
if clause == CLAUSE_SET
|
279
|
+
return apply_clause_set(table)
|
280
|
+
end
|
281
|
+
|
282
|
+
if clause == CLAUSE_NOT_SET
|
283
|
+
return apply_clause_not_set(table)
|
284
|
+
end
|
285
|
+
|
286
|
+
modify_date_and_clause!(input) if is_relative_clause?
|
287
|
+
|
288
|
+
# TODO: Allow for custom clauses
|
289
|
+
if @attribute_type == ATTRIBUTE_TYPE_DATE
|
290
|
+
apply_standardized_values(table)
|
291
|
+
else
|
292
|
+
apply_standardized_values_with_time(table)
|
293
|
+
end
|
294
|
+
end
|
295
|
+
|
296
|
+
def comparison_date(input)
|
297
|
+
modified_days = days.to_i
|
298
|
+
modifier = input[:modifier]
|
299
|
+
|
300
|
+
# If the user has requested a certain number of days 'ago',then value
|
301
|
+
# needs to be negative
|
302
|
+
|
303
|
+
if modifier == "ago"
|
304
|
+
modified_days *= - 1
|
305
|
+
end
|
306
|
+
(Date.current + modified_days).strftime("%Y-%m-%d")
|
307
|
+
end
|
308
|
+
|
309
|
+
def standardize_clause(input)
|
310
|
+
modifier = input[:modifier]
|
311
|
+
case clause
|
312
|
+
when CLAUSE_GREATER_THAN
|
313
|
+
modifier == "ago" ? CLAUSE_LESS_THAN : CLAUSE_GREATER_THAN
|
314
|
+
when CLAUSE_LESS_THAN
|
315
|
+
modifier == "ago" ? CLAUSE_GREATER_THAN : CLAUSE_LESS_THAN
|
316
|
+
when CLAUSE_EXACTLY
|
317
|
+
CLAUSE_EQUALS
|
318
|
+
end
|
319
|
+
end
|
320
|
+
|
321
|
+
def start_of_day(day)
|
322
|
+
# Returns the start of day in the user timezone
|
323
|
+
# Day shifted to user time zone 00 based
|
324
|
+
day_in_user_tz = day.in_time_zone(user_timezone).beginning_of_day
|
325
|
+
|
326
|
+
# Get day_in_user_tz in database time zone
|
327
|
+
database_local = day_in_user_tz.in_time_zone(database_timezone)
|
328
|
+
|
329
|
+
offset = database_local.utc_offset
|
330
|
+
# Arel will convert to UTC before seaching, so add offset to account for db timezone
|
331
|
+
utc_start = database_local.in_time_zone("UTC")
|
332
|
+
utc_start + offset
|
333
|
+
end
|
334
|
+
|
335
|
+
def end_of_day(day)
|
336
|
+
day_in_user_tz = day.in_time_zone(user_timezone)
|
337
|
+
end_of_day = day_in_user_tz.end_of_day
|
338
|
+
database_local = end_of_day.in_time_zone(database_timezone)
|
339
|
+
offset = database_local.utc_offset
|
340
|
+
utc_end = database_local.in_time_zone("UTC")
|
341
|
+
utc_end + offset
|
342
|
+
end
|
343
|
+
|
344
|
+
def comparison_time(day)
|
345
|
+
# If comparison request, compare to the time the request is made (such as 3 days ago)
|
346
|
+
current_time = Time.current.in_time_zone(user_timezone)
|
347
|
+
|
348
|
+
# Day will be 00 based
|
349
|
+
day_in_user_tz = day.to_time(:utc).in_time_zone(user_timezone)
|
350
|
+
|
351
|
+
options = {hour: current_time.hour, min: current_time.min, sec: current_time.sec}
|
352
|
+
|
353
|
+
# The queried day shifted to local time hour::min::sec
|
354
|
+
day_time_shifted = day_in_user_tz.change(options)
|
355
|
+
|
356
|
+
database_local = day_time_shifted.in_time_zone(database_timezone)
|
357
|
+
offset = database_local.utc_offset
|
358
|
+
utc_comparison_time = database_local.in_time_zone("UTC")
|
359
|
+
utc_comparison_time + offset
|
360
|
+
end
|
361
|
+
|
362
|
+
def apply_standardized_values_with_time(table)
|
363
|
+
case clause
|
364
|
+
# At this point, `between` and `equal` are functionally the
|
365
|
+
# same, i.e. they are querying between two _times_.
|
366
|
+
when CLAUSE_EQUALS
|
367
|
+
apply_clause_between(table, start_of_day(date1), end_of_day(date1))
|
368
|
+
when CLAUSE_BETWEEN
|
369
|
+
apply_clause_between(table, start_of_day(date1), end_of_day(date2))
|
370
|
+
|
371
|
+
when CLAUSE_DOESNT_EQUAL
|
372
|
+
apply_clause_not_between(table, start_of_day(date1), end_of_day(date1))
|
373
|
+
when CLAUSE_NOT_BETWEEN
|
374
|
+
apply_clause_not_between(table, start_of_day(date1), end_of_day(date2))
|
375
|
+
|
376
|
+
|
377
|
+
when CLAUSE_LESS_THAN
|
378
|
+
apply_clause_less_than(comparison_time(date1), table)
|
379
|
+
when CLAUSE_GREATER_THAN
|
380
|
+
apply_clause_greater_than(comparison_time(date1), table)
|
381
|
+
when CLAUSE_GREATER_THAN_OR_EQUAL
|
382
|
+
if Refine::Rails.configuration.date_gte_uses_bod
|
383
|
+
datetime = start_of_day(date1)
|
384
|
+
else
|
385
|
+
datetime = comparison_time(date1)
|
386
|
+
end
|
387
|
+
apply_clause_greater_than_or_equal(datetime, table)
|
388
|
+
when CLAUSE_LESS_THAN_OR_EQUAL
|
389
|
+
if Refine::Rails.configuration.date_lte_uses_eod
|
390
|
+
datetime = end_of_day(date1)
|
391
|
+
else
|
392
|
+
datetime = comparison_time(date1)
|
393
|
+
end
|
394
|
+
apply_clause_less_than_or_equal(datetime, table)
|
395
|
+
end
|
396
|
+
|
397
|
+
end
|
398
|
+
|
399
|
+
def apply_standardized_values(table)
|
400
|
+
case clause
|
401
|
+
when CLAUSE_EQUALS
|
402
|
+
apply_clause_equals(date1, table)
|
403
|
+
|
404
|
+
when CLAUSE_DOESNT_EQUAL
|
405
|
+
apply_clause_doesnt_equal(date1, table)
|
406
|
+
|
407
|
+
when CLAUSE_LESS_THAN
|
408
|
+
apply_clause_less_than(date1, table)
|
409
|
+
|
410
|
+
when CLAUSE_GREATER_THAN
|
411
|
+
apply_clause_greater_than(date1, table)
|
412
|
+
|
413
|
+
when CLAUSE_GREATER_THAN_OR_EQUAL
|
414
|
+
apply_clause_greater_than_or_equal(date1, table)
|
415
|
+
|
416
|
+
when CLAUSE_LESS_THAN_OR_EQUAL
|
417
|
+
apply_clause_less_than_or_equal(date1, table)
|
418
|
+
|
419
|
+
when CLAUSE_BETWEEN
|
420
|
+
apply_clause_between(table, date1, date2)
|
421
|
+
end
|
422
|
+
end
|
423
|
+
|
424
|
+
def apply_clause_between(table, first_date, second_date)
|
425
|
+
table.grouping(table[:"#{attribute}"].between(first_date..second_date))
|
426
|
+
end
|
427
|
+
|
428
|
+
def apply_clause_not_between(table, first_date, second_date)
|
429
|
+
table.grouping(table[:"#{attribute}"].not_between(first_date..second_date))
|
430
|
+
end
|
431
|
+
|
432
|
+
def apply_clause_equals(value, table)
|
433
|
+
table.grouping(table[:"#{attribute}"].eq(value))
|
434
|
+
end
|
435
|
+
|
436
|
+
def apply_clause_doesnt_equal(value, table)
|
437
|
+
table.grouping(table[:"#{attribute}"].not_eq(value).or(table[:"#{attribute}"].eq(nil)))
|
438
|
+
end
|
439
|
+
|
440
|
+
def apply_clause_greater_than(value, table)
|
441
|
+
table.grouping(table[:"#{attribute}"].gt(value))
|
442
|
+
end
|
443
|
+
|
444
|
+
def apply_clause_greater_than_or_equal(value, table)
|
445
|
+
table.grouping(table[:"#{attribute}"].gteq(value))
|
446
|
+
end
|
447
|
+
|
448
|
+
def apply_clause_less_than(value, table)
|
449
|
+
table.grouping(table[:"#{attribute}"].lt(value))
|
450
|
+
end
|
451
|
+
|
452
|
+
def apply_clause_less_than_or_equal(value, table)
|
453
|
+
table.grouping(table[:"#{attribute}"].lteq(value))
|
454
|
+
end
|
455
|
+
|
456
|
+
def apply_clause_set(table)
|
457
|
+
table.grouping(table[:"#{attribute}"].not_eq(nil))
|
458
|
+
end
|
459
|
+
|
460
|
+
def apply_clause_not_set(table)
|
461
|
+
table.grouping(table[:"#{attribute}"].eq(nil))
|
462
|
+
end
|
463
|
+
end
|
464
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
class Refine::Conditions::Errors::RelationshipError < StandardError; end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
module Refine::Conditions
|
2
|
+
class FilterCondition < Condition
|
3
|
+
include HasClauses
|
4
|
+
include UsesAttributes
|
5
|
+
include ActiveModel::Validations
|
6
|
+
|
7
|
+
attr_reader :options
|
8
|
+
|
9
|
+
CLAUSE_IN = Clauses::IN
|
10
|
+
CLAUSE_NOT_IN = Clauses::NOT_IN
|
11
|
+
|
12
|
+
I18N_PREFIX = "refine.refine_blueprints.filter_condition."
|
13
|
+
|
14
|
+
def component
|
15
|
+
"filter-condition"
|
16
|
+
end
|
17
|
+
|
18
|
+
def boot
|
19
|
+
@options = nil
|
20
|
+
with_meta({options: get_options})
|
21
|
+
add_ensurance(ensure_options)
|
22
|
+
end
|
23
|
+
|
24
|
+
def set_input_parameters(input)
|
25
|
+
@selected = input[:selected]
|
26
|
+
end
|
27
|
+
|
28
|
+
def select_is_array
|
29
|
+
errors.add(:base, I18n.t("#{I18N_PREFIX}must_be_array")) unless selected.is_a?(Array)
|
30
|
+
end
|
31
|
+
|
32
|
+
def ensure_options
|
33
|
+
proc do
|
34
|
+
developer_options = get_options.call
|
35
|
+
# Options must evaluate to an array
|
36
|
+
if !developer_options.is_a? Array
|
37
|
+
raise I18n.t("#{I18N_PREFIX}options_not_determined")
|
38
|
+
end
|
39
|
+
# Each option must be a hash of values that includes :id and :display
|
40
|
+
developer_options.each do |option|
|
41
|
+
if (!option.is_a? Hash) || option.keys.exclude?(:id) || option.keys.exclude?(:display)
|
42
|
+
raise Refine::Conditions::Errors::OptionError.new(I18n.t("#{I18N_PREFIX}must_have_id_and_display"))
|
43
|
+
end
|
44
|
+
end
|
45
|
+
ensure_no_duplicates(developer_options)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def ensure_no_duplicates(developer_options)
|
50
|
+
id_array = developer_options.map { |option| option[:id] }
|
51
|
+
duplicates = id_array.select { |id| id_array.count(id) > 1 }.uniq
|
52
|
+
if duplicates.present?
|
53
|
+
raise Refine::Conditions::Errors::OptionError.new(I18n.t("#{I18N_PREFIX}must_be_unique", duplicates: duplicates))
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# TODO improve this developer interface...
|
58
|
+
def with_scope(scope)
|
59
|
+
@options = []
|
60
|
+
scope.all.each do |filter|
|
61
|
+
@options << {id: filter.id, display: filter.name}
|
62
|
+
end
|
63
|
+
self
|
64
|
+
end
|
65
|
+
|
66
|
+
def stored_only
|
67
|
+
self
|
68
|
+
end
|
69
|
+
|
70
|
+
def get_options
|
71
|
+
proc do
|
72
|
+
@options = call_proc_if_callable(options)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def apply_condition(input, table, _inverse_clause)
|
77
|
+
filter_id = input[:selected].first.to_i
|
78
|
+
filter = Refine::Rails.configuration.stabilizer_classes[:db].new.from_stable_id(id: filter_id)
|
79
|
+
# TODO handle this more elegantly
|
80
|
+
raise I18n.t("#{I18N_PREFIX}not_found") if filter.blank?
|
81
|
+
# TODO - Filter initial query is currently handled on the filter class. ProductFilter.where....
|
82
|
+
# Is this the right way to handle it?
|
83
|
+
filter.make_sub_query(filter.blueprint)
|
84
|
+
end
|
85
|
+
|
86
|
+
def clauses
|
87
|
+
[
|
88
|
+
Clause.new(CLAUSE_IN, I18n.t("#{I18N_PREFIX}in")),
|
89
|
+
Clause.new(CLAUSE_NOT_IN, I18n.t("#{I18N_PREFIX}not_in"))
|
90
|
+
]
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
module Refine::Conditions
|
2
|
+
module HasClauses
|
3
|
+
|
4
|
+
def self.included(klass)
|
5
|
+
klass.class_eval do
|
6
|
+
mattr_accessor :default_clause_display_map, default: {}, instance_accessor: false
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def boot_has_clauses
|
11
|
+
@show_clauses = {}
|
12
|
+
add_rules({ clause: "required" })
|
13
|
+
with_meta({ clauses: get_clauses })
|
14
|
+
add_ensurance(ensure_clauses)
|
15
|
+
before_validate(before_clause_validation)
|
16
|
+
end
|
17
|
+
|
18
|
+
def clause_display_map
|
19
|
+
@clause_display_map ||= {}
|
20
|
+
end
|
21
|
+
|
22
|
+
def before_clause_validation(input = [])
|
23
|
+
proc do |input|
|
24
|
+
if input.present?
|
25
|
+
current_clause = clauses.select{ |clause| clause.id == input[:clause] }
|
26
|
+
if current_clause.present?
|
27
|
+
add_rules(current_clause[0].rules)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def custom_clauses
|
34
|
+
[]
|
35
|
+
end
|
36
|
+
|
37
|
+
def remap_clause_displays(map)
|
38
|
+
clause_display_map.merge!(map)
|
39
|
+
self
|
40
|
+
end
|
41
|
+
|
42
|
+
def only_clauses(specific_clauses)
|
43
|
+
# Remove all clauses
|
44
|
+
clauses.map(&:id).each {|clause_id| update_show_clauses(clause_id, false) }
|
45
|
+
# Add specific clauses by id, not by fully qualified clause
|
46
|
+
specific_clauses.each {|clause| update_show_clauses(clause, true) }
|
47
|
+
self
|
48
|
+
end
|
49
|
+
|
50
|
+
def with_clauses(clauses_to_include)
|
51
|
+
clauses_to_include.each {|clause| update_show_clauses(clause, true) }
|
52
|
+
self
|
53
|
+
end
|
54
|
+
|
55
|
+
def without_clauses(clauses_to_exclude)
|
56
|
+
clauses_to_exclude.each {|clause| update_show_clauses(clause, false) }
|
57
|
+
self
|
58
|
+
end
|
59
|
+
|
60
|
+
def update_show_clauses(clause, value)
|
61
|
+
@show_clauses.merge!({"#{clause}": value})
|
62
|
+
end
|
63
|
+
|
64
|
+
def ensure_clauses
|
65
|
+
proc do
|
66
|
+
clauses = get_clauses.call
|
67
|
+
if clauses.any?
|
68
|
+
clauses.each { |clause| ensure_clause(clause) }
|
69
|
+
else
|
70
|
+
errors.add(:base, I18n.t("refine.refine_blueprints.has_clauses.not_determined"))
|
71
|
+
raise Errors::ConditionClauseError, "#{errors.full_messages}"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def ensure_clause(clause)
|
77
|
+
if !clause.is_a? Clause
|
78
|
+
errors.add(:base, I18n.t("refine.refine_blueprints.has_clauses.must_be_instance_of", instance: "#{Clause::class}"))
|
79
|
+
raise Errors::ConditionClauseError, "#{errors.full_messages}"
|
80
|
+
end
|
81
|
+
if clause.id.blank? || clause.display.blank?
|
82
|
+
errors.add(:base, I18n.t("refine.refine_blueprints.has_clauses.must_have_id_and_display"))
|
83
|
+
raise Errors::ConditionClauseError, "#{errors.full_messages}"
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def get_clause_by_id(id)
|
88
|
+
clause = get_clauses.call().find{ |clause| clause.id == id }
|
89
|
+
raise I18n.t("refine.refine_blueprints.has_clauses.not_found", id: id) unless clause
|
90
|
+
clause
|
91
|
+
end
|
92
|
+
|
93
|
+
def get_clauses
|
94
|
+
proc do
|
95
|
+
returned_clauses = clauses.dup
|
96
|
+
# Clause display map takes precedence over default display map. Merge order matters.
|
97
|
+
map = self.class.default_clause_display_map.merge(clause_display_map)
|
98
|
+
@show_clauses.each do |clause_id, rule|
|
99
|
+
filterable_clause_index = returned_clauses.index{ |clause| clause.id.to_sym == clause_id }
|
100
|
+
if rule == false
|
101
|
+
returned_clauses.delete_at(filterable_clause_index)
|
102
|
+
elsif rule == true
|
103
|
+
add_clause = returned_clauses.find{|clause| clause.id.to_sym == clause_id }
|
104
|
+
returned_clauses << add_clause if !add_clause
|
105
|
+
end
|
106
|
+
end
|
107
|
+
# Rewrite display if the key exists in the map.
|
108
|
+
returned_clauses.each do |clause|
|
109
|
+
if map.key?(clause.id.to_sym)
|
110
|
+
clause.display = map[clause.id.to_sym]
|
111
|
+
end
|
112
|
+
end
|
113
|
+
returned_clauses
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|