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