refine-rails 2.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (141) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +413 -0
  3. data/Rakefile +8 -0
  4. data/app/assets/config/refine_rails_manifest.js +0 -0
  5. data/app/assets/javascripts/refine-stimulus.esm.js +2 -0
  6. data/app/assets/javascripts/refine-stimulus.esm.js.map +1 -0
  7. data/app/assets/javascripts/refine-stimulus.js +2 -0
  8. data/app/assets/javascripts/refine-stimulus.js.map +1 -0
  9. data/app/assets/javascripts/refine-stimulus.modern.js +2 -0
  10. data/app/assets/javascripts/refine-stimulus.modern.js.map +1 -0
  11. data/app/assets/javascripts/refine-stimulus.umd.js +2 -0
  12. data/app/assets/javascripts/refine-stimulus.umd.js.map +1 -0
  13. data/app/assets/stylesheets/index.css +1873 -0
  14. data/app/assets/stylesheets/index.tailwind.css +1035 -0
  15. data/app/controllers/refine/blueprints_controller.rb +80 -0
  16. data/app/controllers/refine/filter_application_controller.rb +29 -0
  17. data/app/controllers/refine/inline/criteria_controller.rb +161 -0
  18. data/app/controllers/refine/inline/stored_filters_controller.rb +84 -0
  19. data/app/controllers/refine/stored_filters_controller.rb +69 -0
  20. data/app/javascript/controllers/index.js +66 -0
  21. data/app/javascript/controllers/refine/add-controller.js +42 -0
  22. data/app/javascript/controllers/refine/criterion-form-controller.js +31 -0
  23. data/app/javascript/controllers/refine/date-controller.js +113 -0
  24. data/app/javascript/controllers/refine/defaults-controller.js +32 -0
  25. data/app/javascript/controllers/refine/delete-controller.js +13 -0
  26. data/app/javascript/controllers/refine/filter-pills-controller.js +63 -0
  27. data/app/javascript/controllers/refine/form-controller.js +51 -0
  28. data/app/javascript/controllers/refine/inline-conditions-controller.js +33 -0
  29. data/app/javascript/controllers/refine/popup-controller.js +46 -0
  30. data/app/javascript/controllers/refine/search-filter-controller.js +50 -0
  31. data/app/javascript/controllers/refine/server-refresh-controller.js +43 -0
  32. data/app/javascript/controllers/refine/state-controller.js +220 -0
  33. data/app/javascript/controllers/refine/stored-filter-controller.js +23 -0
  34. data/app/javascript/controllers/refine/submit-form-controller.js +8 -0
  35. data/app/javascript/controllers/refine/toggle-controller.js +12 -0
  36. data/app/javascript/controllers/refine/turbo-stream-form-controller.js +24 -0
  37. data/app/javascript/controllers/refine/turbo-stream-link-controller.js +24 -0
  38. data/app/javascript/controllers/refine/update-controller.js +86 -0
  39. data/app/javascript/index.js +1 -0
  40. data/app/javascript/refine/helpers/index.js +77 -0
  41. data/app/models/refine/blueprints/blueprint.rb +58 -0
  42. data/app/models/refine/blueprints/blueprint_example.json +25 -0
  43. data/app/models/refine/conditions/boolean_condition.rb +112 -0
  44. data/app/models/refine/conditions/clause.rb +38 -0
  45. data/app/models/refine/conditions/clauses.rb +38 -0
  46. data/app/models/refine/conditions/condition.rb +285 -0
  47. data/app/models/refine/conditions/condition_error.rb +1 -0
  48. data/app/models/refine/conditions/date_condition.rb +464 -0
  49. data/app/models/refine/conditions/date_with_time_condition.rb +8 -0
  50. data/app/models/refine/conditions/errors/condition_clause_error.rb +7 -0
  51. data/app/models/refine/conditions/errors/criteria_limit_exceeded_error.rb +2 -0
  52. data/app/models/refine/conditions/errors/option_error.rb +2 -0
  53. data/app/models/refine/conditions/errors/relationship_error.rb +1 -0
  54. data/app/models/refine/conditions/filter_condition.rb +93 -0
  55. data/app/models/refine/conditions/has_clauses.rb +117 -0
  56. data/app/models/refine/conditions/has_meta.rb +10 -0
  57. data/app/models/refine/conditions/has_refinements.rb +156 -0
  58. data/app/models/refine/conditions/numeric_condition.rb +224 -0
  59. data/app/models/refine/conditions/option_condition.rb +260 -0
  60. data/app/models/refine/conditions/text_condition.rb +152 -0
  61. data/app/models/refine/conditions/uses_attributes.rb +168 -0
  62. data/app/models/refine/filter.rb +302 -0
  63. data/app/models/refine/filters/blueprint_editor.rb +102 -0
  64. data/app/models/refine/filters/builder.rb +59 -0
  65. data/app/models/refine/filters/criterion.rb +87 -0
  66. data/app/models/refine/filters/query.rb +82 -0
  67. data/app/models/refine/inline/criteria/input.rb +50 -0
  68. data/app/models/refine/inline/criteria/numeric_refinement.rb +13 -0
  69. data/app/models/refine/inline/criteria/option.rb +2 -0
  70. data/app/models/refine/inline/criterion.rb +141 -0
  71. data/app/models/refine/invalid_filter_error.rb +8 -0
  72. data/app/models/refine/stabilize.rb +29 -0
  73. data/app/models/refine/stabilizers/database_stabilizer.rb +21 -0
  74. data/app/models/refine/stabilizers/errors/url_stabilizer_error.rb +2 -0
  75. data/app/models/refine/stabilizers/url_encoded_stabilizer.rb +21 -0
  76. data/app/models/refine/stored_filter.rb +14 -0
  77. data/app/models/refine/tracks_pending_relationship_subqueries.rb +196 -0
  78. data/app/views/_filter_builder_dropdown.html.erb +63 -0
  79. data/app/views/_filter_pills.html.erb +40 -0
  80. data/app/views/_loading.html.erb +32 -0
  81. data/app/views/refine/blueprints/_add_and.html.erb +25 -0
  82. data/app/views/refine/blueprints/_add_group.html.erb +24 -0
  83. data/app/views/refine/blueprints/_clause_select.html.erb +24 -0
  84. data/app/views/refine/blueprints/_condition_select.html.erb +53 -0
  85. data/app/views/refine/blueprints/_criterion.html.erb +41 -0
  86. data/app/views/refine/blueprints/_criterion_errors.html.erb +7 -0
  87. data/app/views/refine/blueprints/_delete_criterion.html.erb +11 -0
  88. data/app/views/refine/blueprints/_group.html.erb +13 -0
  89. data/app/views/refine/blueprints/_query.html.erb +34 -0
  90. data/app/views/refine/blueprints/_stored_filters.html.erb +23 -0
  91. data/app/views/refine/blueprints/clauses/_date_condition.html.erb +80 -0
  92. data/app/views/refine/blueprints/clauses/_date_picker.html.erb +26 -0
  93. data/app/views/refine/blueprints/clauses/_filter_condition.html.erb +36 -0
  94. data/app/views/refine/blueprints/clauses/_numeric_condition.html.erb +35 -0
  95. data/app/views/refine/blueprints/clauses/_option_condition.html.erb +37 -0
  96. data/app/views/refine/blueprints/clauses/_text_condition.html.erb +13 -0
  97. data/app/views/refine/blueprints/create.turbo_stream.erb +22 -0
  98. data/app/views/refine/blueprints/new.html.erb +7 -0
  99. data/app/views/refine/blueprints/show.html.erb +4 -0
  100. data/app/views/refine/blueprints/show.turbo_stream.erb +22 -0
  101. data/app/views/refine/inline/criteria/_form_fields.html.erb +62 -0
  102. data/app/views/refine/inline/criteria/create.turbo_stream.erb +19 -0
  103. data/app/views/refine/inline/criteria/edit.turbo_stream.erb +26 -0
  104. data/app/views/refine/inline/criteria/index.html.erb +64 -0
  105. data/app/views/refine/inline/criteria/new.turbo_stream.erb +24 -0
  106. data/app/views/refine/inline/filters/_add_first_condition_button.html.erb +19 -0
  107. data/app/views/refine/inline/filters/_and_button.html.erb +26 -0
  108. data/app/views/refine/inline/filters/_criterion.html.erb +23 -0
  109. data/app/views/refine/inline/filters/_group.html.erb +13 -0
  110. data/app/views/refine/inline/filters/_load_button.html.erb +15 -0
  111. data/app/views/refine/inline/filters/_or_button.html.erb +26 -0
  112. data/app/views/refine/inline/filters/_popup.html.erb +26 -0
  113. data/app/views/refine/inline/filters/_save_button.html.erb +15 -0
  114. data/app/views/refine/inline/filters/_show.html.erb +40 -0
  115. data/app/views/refine/inline/inputs/_date_condition.html.erb +7 -0
  116. data/app/views/refine/inline/inputs/_date_condition_days.html.erb +18 -0
  117. data/app/views/refine/inline/inputs/_date_condition_range.html.erb +22 -0
  118. data/app/views/refine/inline/inputs/_date_condition_single.html.erb +9 -0
  119. data/app/views/refine/inline/inputs/_date_picker.html.erb +20 -0
  120. data/app/views/refine/inline/inputs/_numeric_condition.html.erb +23 -0
  121. data/app/views/refine/inline/inputs/_option_condition.html.erb +14 -0
  122. data/app/views/refine/inline/inputs/_text_condition.html.erb +8 -0
  123. data/app/views/refine/inline/stored_filters/find.turbo_stream.erb +19 -0
  124. data/app/views/refine/inline/stored_filters/index.html.erb +28 -0
  125. data/app/views/refine/inline/stored_filters/new.turbo_stream.erb +47 -0
  126. data/app/views/refine/stored_filters/create.turbo_stream.erb +2 -0
  127. data/app/views/refine/stored_filters/find.turbo_stream.erb +5 -0
  128. data/app/views/refine/stored_filters/index.html.erb +39 -0
  129. data/app/views/refine/stored_filters/new.html.erb +29 -0
  130. data/app/views/refine/stored_filters/show.html.erb +1 -0
  131. data/config/locales/en/dates.en.yml +29 -0
  132. data/config/locales/en/en.yml +20 -0
  133. data/config/locales/en/refine.en.yml +187 -0
  134. data/config/routes.rb +17 -0
  135. data/lib/generators/filter/filter_generator.rb +27 -0
  136. data/lib/generators/filter/templates/filter.rb.erb +20 -0
  137. data/lib/refine/rails/engine.rb +15 -0
  138. data/lib/refine/rails/version.rb +5 -0
  139. data/lib/refine/rails.rb +38 -0
  140. data/lib/tasks/refine/rails_tasks.rake +13 -0
  141. 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,8 @@
1
+ module Refine::Conditions
2
+ class DateWithTimeCondition < DateCondition
3
+ def boot
4
+ @attribute_type = ATTRIBUTE_TYPE_DATE_WITH_TIME
5
+ super
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,7 @@
1
+ class Refine::Conditions::Errors::ConditionClauseError < StandardError
2
+ attr_reader :errors
3
+ def initialize(message, errors: [])
4
+ @errors = errors
5
+ super(message)
6
+ end
7
+ end
@@ -0,0 +1,2 @@
1
+ class Refine::Conditions::Errors::CriteriaLimitExceededError < RuntimeError
2
+ end
@@ -0,0 +1,2 @@
1
+ class Refine::Conditions::Errors::OptionError < RuntimeError
2
+ 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