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