eluvia-base 3.37.1

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 (45) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +225 -0
  4. data/lib/eluvia/active_model/amount_attribute.rb +108 -0
  5. data/lib/eluvia/active_model/array_attribute.rb +134 -0
  6. data/lib/eluvia/active_model/attributes.rb +417 -0
  7. data/lib/eluvia/active_model/enum_array_attribute.rb +60 -0
  8. data/lib/eluvia/active_model/enum_attribute.rb +174 -0
  9. data/lib/eluvia/active_model/object_attribute.rb +143 -0
  10. data/lib/eluvia/active_model/range_attribute.rb +74 -0
  11. data/lib/eluvia/active_record/file_array_attribute.rb +116 -0
  12. data/lib/eluvia/active_record/file_attribute.rb +158 -0
  13. data/lib/eluvia/active_record/filtering.rb +680 -0
  14. data/lib/eluvia/active_record/filters_model.rb +58 -0
  15. data/lib/eluvia/active_record/ordering.rb +110 -0
  16. data/lib/eluvia/base/config.rb +38 -0
  17. data/lib/eluvia/base/version.rb +12 -0
  18. data/lib/eluvia/errors/bad_request.rb +17 -0
  19. data/lib/eluvia/errors/forbidden.rb +17 -0
  20. data/lib/eluvia/errors/not_found.rb +17 -0
  21. data/lib/eluvia/errors/service_unavailable.rb +17 -0
  22. data/lib/eluvia/errors/standard_error.rb +33 -0
  23. data/lib/eluvia/errors/unauthorized.rb +17 -0
  24. data/lib/eluvia/errors/unprocessable_entity.rb +26 -0
  25. data/lib/eluvia/fieldset/rest_field.rb +83 -0
  26. data/lib/eluvia/fieldset/rest_fieldset.rb +158 -0
  27. data/lib/eluvia/fieldset.rb +12 -0
  28. data/lib/eluvia/handlers/error_handler.rb +60 -0
  29. data/lib/eluvia/handlers/pagination_handler.rb +25 -0
  30. data/lib/eluvia/handlers/params_handler.rb +43 -0
  31. data/lib/eluvia/helpers/attachment_helper.rb +37 -0
  32. data/lib/eluvia/integrations/eluvia_integration.rb +281 -0
  33. data/lib/eluvia/models/file.rb +44 -0
  34. data/lib/eluvia/models/file_wrapper.rb +38 -0
  35. data/lib/eluvia/models/image_wrapper.rb +38 -0
  36. data/lib/eluvia/serializers/error_serializer.rb +21 -0
  37. data/lib/eluvia/serializers/fieldset_serializer.rb +63 -0
  38. data/lib/eluvia/services/health_service.rb +56 -0
  39. data/lib/eluvia/uploads.rb +13 -0
  40. data/lib/eluvia/utils/hash_and_array.rb +98 -0
  41. data/lib/eluvia/utils/jwt.rb +36 -0
  42. data/lib/eluvia/utils/string.rb +31 -0
  43. data/lib/eluvia/utils/uuid.rb +21 -0
  44. data/lib/eluvia-base.rb +79 -0
  45. metadata +186 -0
@@ -0,0 +1,680 @@
1
+ module Eluvia
2
+ module ActiveRecord
3
+ module Filtering
4
+ extend ::ActiveSupport::Concern
5
+
6
+ UUID_FORMAT = /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i
7
+
8
+ class_methods do
9
+
10
+ def filter_2(params = {}, current_user: nil)
11
+ p = params.to_h.symbolize_keys
12
+ p = parse_filter_term(p[:filter].to_s, raise_on_invalid: false) if p.key?(:filter)
13
+ apply_filters(self.all, p, current_user: current_user)
14
+ end
15
+
16
+ def filter_2!(params = {}, current_user: nil)
17
+ p = params.to_h.symbolize_keys
18
+ if p.key?(:filter)
19
+ p = parse_filter_term(p[:filter].to_s, raise_on_invalid: true) # raises error on invalid filter param
20
+ apply_filters(self.all, p, current_user: current_user, prefix: 'filter__')
21
+ else
22
+ p.each_key do |k|
23
+ unless permitted_filter_params.include?(k)
24
+ raise Eluvia::Errors::UnprocessableEntity.new({
25
+ k => I18n.t('eluvia.errors.filter.unknown_param', default: 'Unknown filter param.')
26
+ })
27
+ end
28
+ end
29
+ apply_filters(self.all, p, current_user: current_user)
30
+ end
31
+ end
32
+
33
+ def search(search_term)
34
+ apply_search(self.all, search_term)
35
+ end
36
+
37
+ def apply_filters(scope, params, parent: nil, current_user: nil, prefix: '')
38
+ return scope if filters_model_class.nil?
39
+
40
+ # Apply filters by attribute type
41
+ p = params.to_h.symbolize_keys
42
+ scope = apply_search(scope, p[:search]) if p.key?(:search)
43
+ scope = apply_filters_for_filter_attrs(scope, p, parent: parent, current_user: current_user)
44
+ scope = apply_filters_for_has_one_attrs(scope, p, current_user: current_user)
45
+ scope = apply_filters_for_range_attrs(scope, p, prefix: prefix)
46
+ scope = apply_filters_for_uuid_attrs(scope, p, prefix: prefix)
47
+ scope = apply_filters_for_string_attrs(scope, p)
48
+ scope = apply_filters_for_integer_attrs(scope, p, prefix: prefix)
49
+ scope = apply_filters_for_date_attrs(scope, p, prefix: prefix)
50
+ apply_filters_for_boolean_attrs(scope, p)
51
+
52
+ # Return final scope
53
+
54
+ end
55
+
56
+ # Returns hash of defined filter params together with typing information and human labels
57
+ def filter_params
58
+ @filter_params ||= _filter_params
59
+ end
60
+
61
+ def _filter_params
62
+ result = {}
63
+ return result unless is_filters_model?(filters_model_class)
64
+ used_attr_names = {}
65
+
66
+ # Custom filter attr are handled with the highest priority
67
+ filters_model_class.filter_attrs.each do |attr_spec|
68
+ attr_name = attr_spec[:attr_name]
69
+ base = {
70
+ attr_name: attr_name,
71
+ model: filters_model_class,
72
+ type: attr_spec[:type],
73
+ label: filters_model_class.human_attribute_name(attr_name)
74
+ }
75
+ result[attr_name] = base.merge({ operator: :eq })
76
+ used_attr_names[attr_name] = true
77
+ end
78
+
79
+ # Handle has_one nested filters
80
+ filters_model_class.attrs(type: :has_one).each do |attr_spec|
81
+ next if used_attr_names.key?(attr_spec[:attr_name]) # skip if already used
82
+ begin
83
+ attr_name = attr_spec[:attr_name]
84
+
85
+ # Add base filter param expecting referenced object ID
86
+ base = {
87
+ attr_name: attr_name,
88
+ model: filters_model_class,
89
+ type: :uuid,
90
+ label: filters_model_class.human_attribute_name(attr_name)
91
+ }
92
+ result[attr_name] = base.merge({ operator: :eq })
93
+
94
+ # Add all nested filter params
95
+ filters_class_name = attr_spec[:class_name]
96
+ model_class_name = filters_class_name.delete_suffix("::Filters")
97
+ model_class = model_class_name.constantize
98
+ nested_filter_params = model_class.filter_params
99
+ nested_filter_params.each do |k, v|
100
+ result[:"#{attr_name}__#{k}"] = v
101
+ end
102
+
103
+ used_attr_names[attr_name] = true
104
+ rescue NameError
105
+ # Ignore if class not found
106
+ end
107
+ end
108
+
109
+ # Handle range attribute type
110
+ filters_model_class.range_attr_names.each do |attr_name|
111
+ next if used_attr_names.key?(attr_name) # skip if already used
112
+ base = {
113
+ attr_name: attr_name,
114
+ model: filters_model_class,
115
+ type: :range,
116
+ label: filters_model_class.human_attribute_name(attr_name)
117
+ }
118
+ result[attr_name] = base.merge({ operator: :eq })
119
+ result[:"#{attr_name}__overlap"] = base.merge({ operator: :overlap })
120
+ used_attr_names[attr_name] = true
121
+ end
122
+
123
+ # Handle simple attribute types - uuid
124
+ filters_model_class.attr_names(type: :uuid).each do |attr_name|
125
+ next if used_attr_names.key?(attr_name) # skip if already used
126
+ base = {
127
+ attr_name: attr_name,
128
+ model: filters_model_class,
129
+ type: :uuid,
130
+ label: filters_model_class.human_attribute_name(attr_name)
131
+ }
132
+ result[attr_name] = base.merge({ operator: :eq })
133
+ result[:"#{attr_name}__in"] = base.merge({ operator: :in })
134
+ used_attr_names[attr_name] = true
135
+ end
136
+
137
+ # Handle simple attribute types - string
138
+ filters_model_class.attr_names(type: :string).each do |attr_name|
139
+ next if used_attr_names.key?(attr_name) # skip if already used
140
+ base = {
141
+ attr_name: attr_name,
142
+ model: filters_model_class,
143
+ type: :string,
144
+ label: filters_model_class.human_attribute_name(attr_name)
145
+ }
146
+ result[attr_name] = base.merge({ operator: :eq })
147
+ result[:"#{attr_name}__gte"] = base.merge({ operator: :gte })
148
+ result[:"#{attr_name}__gt"] = base.merge({ operator: :gt })
149
+ result[:"#{attr_name}__lte"] = base.merge({ operator: :lte })
150
+ result[:"#{attr_name}__lt"] = base.merge({ operator: :lt })
151
+ result[:"#{attr_name}__icontains"] = base.merge({ operator: :icontains })
152
+ result[:"#{attr_name}__in"] = base.merge({ operator: :in })
153
+ used_attr_names[attr_name] = true
154
+ end
155
+
156
+ # Handle simple attribute types - integer
157
+ filters_model_class.attr_names(type: :integer).each do |attr_name|
158
+ next if used_attr_names.key?(attr_name) # skip if already used
159
+ base = {
160
+ attr_name: attr_name,
161
+ model: filters_model_class,
162
+ type: :integer,
163
+ label: filters_model_class.human_attribute_name(attr_name)
164
+ }
165
+ result[attr_name] = base.merge({ operator: :eq })
166
+ result[:"#{attr_name}__gte"] = base.merge({ operator: :gte })
167
+ result[:"#{attr_name}__gt"] = base.merge({ operator: :gt })
168
+ result[:"#{attr_name}__lte"] = base.merge({ operator: :lte })
169
+ result[:"#{attr_name}__lt"] = base.merge({ operator: :lt })
170
+ used_attr_names[attr_name] = true
171
+ end
172
+
173
+ # Handle simple attribute types - date
174
+ filters_model_class.attr_names(type: :date).each do |attr_name|
175
+ next if used_attr_names.key?(attr_name) # skip if already used
176
+ base = {
177
+ attr_name: attr_name,
178
+ model: filters_model_class,
179
+ type: :date,
180
+ label: filters_model_class.human_attribute_name(attr_name)
181
+ }
182
+ result[attr_name] = base.merge({ operator: :eq })
183
+ result[:"#{attr_name}__from"] = base.merge({ operator: :from })
184
+ result[:"#{attr_name}__to"] = base.merge({ operator: :to })
185
+ used_attr_names[attr_name] = true
186
+ end
187
+
188
+ # Handle simple attribute types - boolean
189
+ filters_model_class.attr_names(type: :boolean).each do |attr_name|
190
+ next if used_attr_names.key?(attr_name) # skip if already used
191
+ base = {
192
+ attr_name: attr_name,
193
+ model: filters_model_class,
194
+ type: :boolean,
195
+ label: filters_model_class.human_attribute_name(attr_name)
196
+ }
197
+ result[attr_name] = base.merge({ operator: :eq })
198
+ used_attr_names[attr_name] = true
199
+ end
200
+
201
+ result
202
+ end
203
+
204
+ # Returns array of permitted filter params usable for "strong parameters" mechanism
205
+ def permitted_filter_params
206
+ if @permitted_filter_params.nil?
207
+ @permitted_filter_params = _permitted_filter_params
208
+ @permitted_filter_params += %i[filter search] # support for a generic "filter" and "search" params
209
+ end
210
+ @permitted_filter_params
211
+ end
212
+
213
+ # Returns array of permitted filter params usable for "strong parameters" mechanism
214
+ def _permitted_filter_params
215
+ result = []
216
+ return result unless is_filters_model?(filters_model_class)
217
+ used_attr_names = {}
218
+
219
+ # Custom filter attr are handled with the highest priority
220
+ filters_model_class.filter_attrs.each do |attr_spec|
221
+ result << attr_spec[:attr_name]
222
+ used_attr_names[attr_spec[:attr_name]] = true
223
+ end
224
+
225
+ # Handle has_one nested filters
226
+ filters_model_class.attrs(type: :has_one).each do |attr_spec|
227
+ next if used_attr_names.key?(attr_spec[:attr_name]) # skip if already used
228
+ begin
229
+ attr_name = attr_spec[:attr_name]
230
+
231
+ # Add base filter param expecting referenced object ID
232
+ result << attr_name
233
+
234
+ # Add all nested filter params
235
+ filters_class_name = attr_spec[:class_name]
236
+ model_class_name = filters_class_name.delete_suffix("::Filters")
237
+ model_class = model_class_name.constantize
238
+ nested_permitted_filter_params = model_class.permitted_filter_params
239
+ nested_permitted_filter_params.each do |nested_attr_name|
240
+ if nested_attr_name.is_a?(Hash)
241
+ result << { "#{attr_name}__#{nested_attr_name.keys.first}": nested_attr_name.values.first }
242
+ else
243
+ result << :"#{attr_name}__#{nested_attr_name}"
244
+ end
245
+ end
246
+ used_attr_names[attr_name] = true
247
+ rescue NameError
248
+ # Ignore if class not found
249
+ end
250
+ end
251
+
252
+ # Handle range attribute type
253
+ filters_model_class.range_attr_names.each do |attr_name|
254
+ next if used_attr_names.key?(attr_name) # skip if already used
255
+ result << { attr_name => %i[lower upper] }
256
+ result << { "#{attr_name}__overlap": %i[lower upper] }
257
+ used_attr_names[attr_name] = true
258
+ end
259
+
260
+ # Handle simple attribute types - uuid
261
+ filters_model_class.attr_names(type: :uuid).each do |attr_name|
262
+ next if used_attr_names.key?(attr_name) # skip if already used
263
+ result << attr_name
264
+ result << :"#{attr_name}__in"
265
+ result << { "#{attr_name}__in": [] }
266
+ used_attr_names[attr_name] = true
267
+ end
268
+
269
+ # Handle simple attribute types - string
270
+ filters_model_class.attr_names(type: :string).each do |attr_name|
271
+ next if used_attr_names.key?(attr_name) # skip if already used
272
+ result << attr_name
273
+ result << :"#{attr_name}__gte"
274
+ result << :"#{attr_name}__gt"
275
+ result << :"#{attr_name}__lte"
276
+ result << :"#{attr_name}__lt"
277
+ result << :"#{attr_name}__icontains"
278
+ result << :"#{attr_name}__in"
279
+ result << { "#{attr_name}__in": [] }
280
+ used_attr_names[attr_name] = true
281
+ end
282
+
283
+ # Handle simple attribute types - integer
284
+ filters_model_class.attr_names(type: :integer).each do |attr_name|
285
+ next if used_attr_names.key?(attr_name) # skip if already used
286
+ result << attr_name
287
+ result << :"#{attr_name}__gte"
288
+ result << :"#{attr_name}__gt"
289
+ result << :"#{attr_name}__lte"
290
+ result << :"#{attr_name}__lt"
291
+ used_attr_names[attr_name] = true
292
+ end
293
+
294
+ # Handle simple attribute types - date
295
+ filters_model_class.attr_names(type: :date).each do |attr_name|
296
+ next if used_attr_names.key?(attr_name) # skip if already used
297
+ result << attr_name
298
+ result << :"#{attr_name}__from"
299
+ result << :"#{attr_name}__to"
300
+ used_attr_names[attr_name] = true
301
+ end
302
+
303
+ # Handle simple attribute types - boolean
304
+ filters_model_class.attr_names(type: :boolean).each do |attr_name|
305
+ next if used_attr_names.key?(attr_name) # skip if already used
306
+ result << attr_name
307
+ used_attr_names[attr_name] = true
308
+ end
309
+
310
+ result
311
+ end
312
+
313
+ def parse_filter_term(filter_term, raise_on_invalid: true)
314
+ result = {}
315
+ return result if filter_term.blank?
316
+
317
+ filter_term.split('&').map do |term|
318
+ term = term.trim
319
+ if term.blank?
320
+ if raise_on_invalid
321
+ raise Eluvia::Errors::UnprocessableEntity.new({
322
+ "filter" => I18n.t('eluvia.errors.filter.invalid_term', default: 'Invalid filter term.')
323
+ })
324
+ else
325
+ next
326
+ end
327
+ end
328
+
329
+ # Parse individual term
330
+ begin
331
+ field_name, value = term.split('=', 2) # split values by '='
332
+ value = JSON.parse(value) # load values from the JSON format (only list, int, strings should be allowed)
333
+ rescue JSON::ParserError, TypeError
334
+ if raise_on_invalid
335
+ raise Eluvia::Errors::UnprocessableEntity.new({
336
+ "filter__#{field_name}" => I18n.t('eluvia.errors.filter.invalid_term',
337
+ default: 'Invalid filter term.')
338
+ })
339
+ else
340
+ next
341
+ end
342
+ end
343
+
344
+ # Check if field can be filtered
345
+ permitted_filter_keys = permitted_filter_params.map { |fp| fp.is_a?(Hash) ? fp.keys.first : fp }
346
+ unless permitted_filter_keys.include?(field_name.to_sym)
347
+ if raise_on_invalid
348
+ raise Eluvia::Errors::UnprocessableEntity.new({
349
+ "filter__#{field_name}" => I18n.t('eluvia.errors.filter.unknown_param',
350
+ default: 'Unknown filter param.')
351
+ })
352
+ else
353
+ next
354
+ end
355
+ end
356
+
357
+ result[field_name] = value
358
+ end
359
+ result
360
+ end
361
+
362
+ protected
363
+
364
+ def apply_search(scope, search_term)
365
+ return scope if search_term.blank?
366
+ return scope if filters_model_class.nil?
367
+
368
+ fields = filters_model_class.search_fields
369
+ return scope if fields.empty?
370
+
371
+ search_term.split.each do |word|
372
+ sql_parts = fields.map do |field|
373
+ col = field.is_a?(Symbol) ? "#{self.table_name}.#{field}" : field.to_s
374
+ "lower(unaccent(trim(#{col}))) LIKE ('%' || lower(unaccent(trim(?))) || '%')"
375
+ end
376
+ scope = scope.where(sql_parts.join(' OR '), *([word] * fields.size))
377
+ end
378
+ scope
379
+ end
380
+
381
+ def apply_filters_for_filter_attrs(scope, params, parent: nil, current_user: nil)
382
+ return scope unless is_filters_model?(filters_model_class)
383
+ filters_model_class.filter_attrs.each do |attr_spec|
384
+ attr_name = attr_spec[:attr_name]
385
+ value = params[attr_name]
386
+ next if value.blank?
387
+ proc = attr_spec[:proc]
388
+ next unless proc.respond_to?(:call)
389
+ scope = proc.call(
390
+ filters_model_class.clean_value(value, attr_name), # clean value passed to the filtering procedure
391
+ scope,
392
+ parent: parent,
393
+ current_user: current_user
394
+ )
395
+ end
396
+ scope
397
+ end
398
+
399
+ def apply_filters_for_has_one_attrs(scope, params, current_user: nil)
400
+ return scope unless is_filters_model?(filters_model_class)
401
+ filters_model_class.attrs(type: :has_one).each do |attr_spec|
402
+ next if filters_model_class.has_filter_attr?(attr_spec[:attr_name])
403
+ attr_name = attr_spec[:attr_name]
404
+ class_name = attr_spec[:class_name]
405
+ scope = apply_filters_for_has_one_attr(scope, attr_name, class_name, params, current_user: current_user)
406
+ end
407
+ scope
408
+ end
409
+
410
+ def apply_filters_for_has_one_attr(scope, attr_name, filters_class_name, params, current_user: nil)
411
+
412
+ # Model of the nested filters
413
+ model_class_name = filters_class_name.delete_suffix("::Filters")
414
+ model_class = model_class_name.constantize
415
+
416
+ # Exact match on the referenced object ID
417
+ unless params[attr_name].blank?
418
+ scope = scope.joins(attr_name.to_sym) # presume the "belongs_to" or "has_one" relation is named same as the filter attribute
419
+ scope = scope.where("#{model_class.table_name}.id = ?", params[attr_name])
420
+ end
421
+
422
+ # Collect nested params applicable for the nested filters
423
+ nested_params = {}
424
+ model_class.permitted_filter_params.each do |nested_attr_name|
425
+ param_key = :"#{attr_name}__#{nested_attr_name}"
426
+ unless params[param_key].blank?
427
+ nested_params[nested_attr_name] = params[param_key]
428
+ end
429
+ end
430
+
431
+ # Apply nested filters, we can join one level deep only for now
432
+ unless nested_params.empty?
433
+ scope = scope.joins(attr_name.to_sym) # presume the "belongs_to" or "has_one" relation is named same as the filter attribute
434
+ scope = model_class.apply_filters(scope, nested_params, parent: attr_name.to_sym, current_user: current_user)
435
+ end
436
+ scope
437
+ rescue NameError
438
+ scope
439
+ end
440
+
441
+ def apply_filters_for_range_attrs(scope, params, prefix: '')
442
+ return scope unless is_filters_model?(filters_model_class)
443
+ filters_model_class.range_attrs.each do |attr_spec|
444
+ next if filters_model_class.has_filter_attr?(attr_spec[:attr_name])
445
+ attr_name = attr_spec[:attr_name]
446
+ lower_attr_name = attr_spec[:lower_attr_name]
447
+ upper_attr_name = attr_spec[:upper_attr_name]
448
+
449
+ # Exact match
450
+ unless params[attr_name].blank?
451
+ value = begin
452
+ filters_model_class.clean_range_value(params[attr_name], attr_name)
453
+ rescue Eluvia::Errors::StandardError
454
+ raise Eluvia::Errors::UnprocessableEntity.new({
455
+ "#{prefix}#{attr_name}" => I18n.t('eluvia.errors.filter.invalid_value_format',
456
+ default: 'Invalid value format.')
457
+ })
458
+ end
459
+ if value
460
+ if value[:lower]
461
+ scope = scope.where("#{self.table_name}.#{lower_attr_name} = ?", value[:lower])
462
+ else
463
+ scope = scope.where("#{self.table_name}.#{lower_attr_name} IS NULL")
464
+ end
465
+ if value[:upper]
466
+ scope = scope.where("#{self.table_name}.#{upper_attr_name} = ?", value[:upper])
467
+ else
468
+ scope = scope.where("#{self.table_name}.#{upper_attr_name} IS NULL")
469
+ end
470
+ end
471
+ end
472
+
473
+ # Overlap match
474
+ attr_name_overlap = :"#{attr_name}__overlap"
475
+ unless params[attr_name_overlap].blank?
476
+ value = begin
477
+ filters_model_class.clean_range_value(params[attr_name_overlap], attr_name)
478
+ rescue Eluvia::Errors::StandardError
479
+ raise Eluvia::Errors::UnprocessableEntity.new({
480
+ "#{prefix}#{attr_name_overlap}" => I18n.t('eluvia.errors.filter.invalid_value_format',
481
+ default: 'Invalid value format.')
482
+ })
483
+ end
484
+ if value
485
+ if value[:lower]
486
+ scope = scope.where("(#{self.table_name}.#{upper_attr_name} IS NULL OR #{self.table_name}.#{upper_attr_name} >= ?)", value[:lower])
487
+ end
488
+ if value[:upper]
489
+ scope = scope.where("(#{self.table_name}.#{lower_attr_name} IS NULL OR #{self.table_name}.#{lower_attr_name} <= ?)", value[:upper])
490
+ end
491
+ end
492
+ end
493
+
494
+ end
495
+ scope
496
+ end
497
+
498
+ def apply_filters_for_uuid_attrs(scope, params, prefix: '')
499
+ return scope unless is_filters_model?(filters_model_class)
500
+ filters_model_class.attrs(type: :uuid).each do |attr_spec|
501
+ next if filters_model_class.has_filter_attr?(attr_spec[:attr_name])
502
+ attr_name = attr_spec[:attr_name]
503
+ underlying_attr_name = attr_spec[:underlying_attr_name] || attr_name
504
+
505
+ # Exact match
506
+ unless params[attr_name].blank?
507
+ unless params[attr_name].to_s.match?(UUID_FORMAT)
508
+ raise Eluvia::Errors::UnprocessableEntity.new({
509
+ "#{prefix}#{attr_name}" => I18n.t('eluvia.errors.filter.invalid_uuid_format',
510
+ default: 'Invalid UUID format.')
511
+ })
512
+ end
513
+ scope = scope.where("#{self.table_name}.#{underlying_attr_name} = ?", filters_model_class.clean_and_transform_value(params[attr_name], attr_name))
514
+ end
515
+
516
+ # In match
517
+ attr_name_in = :"#{attr_name}__in"
518
+ unless params[attr_name_in].nil? # we want to support empty array as well
519
+ values = params[attr_name_in]
520
+ values = values.to_s.split(',') unless values.is_a?(Array)
521
+ values.each do |v|
522
+ unless v.to_s.match?(UUID_FORMAT)
523
+ raise Eluvia::Errors::UnprocessableEntity.new({
524
+ "#{prefix}#{attr_name_in}" => I18n.t('eluvia.errors.filter.invalid_uuid_format',
525
+ default: 'Invalid UUID format.')
526
+ })
527
+ end
528
+ end
529
+ scope = scope.where("#{self.table_name}.#{underlying_attr_name}" => values.map { |v| filters_model_class.clean_and_transform_value(v, attr_name) })
530
+ end
531
+ end
532
+ scope
533
+ end
534
+
535
+ def apply_filters_for_string_attrs(scope, params)
536
+ return scope unless is_filters_model?(filters_model_class)
537
+ filters_model_class.attrs(type: :string).each do |attr_spec|
538
+ next if filters_model_class.has_filter_attr?(attr_spec[:attr_name])
539
+ attr_name = attr_spec[:attr_name]
540
+ underlying_attr_name = attr_spec[:underlying_attr_name] || attr_name
541
+
542
+ # Exact match
543
+ unless params[attr_name].blank?
544
+ scope = scope.where("#{self.table_name}.#{underlying_attr_name} = ?", filters_model_class.clean_and_transform_value(params[attr_name], attr_name))
545
+ end
546
+
547
+ # Greater than or equal match
548
+ attr_name_gte = :"#{attr_name}__gte"
549
+ unless params[attr_name_gte].blank?
550
+ scope = scope.where("#{self.table_name}.#{underlying_attr_name} >= ?", filters_model_class.clean_and_transform_value(params[attr_name_gte], attr_name))
551
+ end
552
+
553
+ # Greater than match
554
+ attr_name_gt = :"#{attr_name}__gt"
555
+ unless params[attr_name_gt].blank?
556
+ scope = scope.where("#{self.table_name}.#{underlying_attr_name} > ?", filters_model_class.clean_and_transform_value(params[attr_name_gt], attr_name))
557
+ end
558
+
559
+ # Less than or equal match
560
+ attr_name_lte = :"#{attr_name}__lte"
561
+ unless params[attr_name_lte].blank?
562
+ scope = scope.where("#{self.table_name}.#{underlying_attr_name} <= ?", filters_model_class.clean_and_transform_value(params[attr_name_lte], attr_name))
563
+ end
564
+
565
+ # Less than match
566
+ attr_name_lt = :"#{attr_name}__lt"
567
+ unless params[attr_name_lt].blank?
568
+ scope = scope.where("#{self.table_name}.#{underlying_attr_name} < ?", filters_model_class.clean_and_transform_value(params[attr_name_lt], attr_name))
569
+ end
570
+
571
+ # Icontains match
572
+ attr_name_icontains = :"#{attr_name}__icontains"
573
+ unless params[attr_name_icontains].blank?
574
+ scope = scope.where("lower(#{self.table_name}.#{underlying_attr_name}) LIKE ('%' || lower(trim(?)) || '%')",
575
+ filters_model_class.clean_and_transform_value(params[attr_name_icontains], attr_name))
576
+ end
577
+
578
+ # In match
579
+ attr_name_in = :"#{attr_name}__in"
580
+ unless params[attr_name_in].nil? # we want to support empty array as well
581
+ values = params[attr_name_in]
582
+ values = values.to_s.split(',') unless values.is_a?(Array)
583
+ scope = scope.where("#{self.table_name}.#{underlying_attr_name}" => values.map { |v| filters_model_class.clean_and_transform_value(v, attr_name) })
584
+ end
585
+ end
586
+ scope
587
+ end
588
+
589
+ def apply_filters_for_integer_attrs(scope, params, prefix: '')
590
+ return scope unless is_filters_model?(filters_model_class)
591
+ filters_model_class.attrs(type: :integer).each do |attr_spec|
592
+ next if filters_model_class.has_filter_attr?(attr_spec[:attr_name])
593
+ attr_name = attr_spec[:attr_name]
594
+ underlying_attr_name = attr_spec[:underlying_attr_name] || attr_name
595
+
596
+ [
597
+ [attr_name, "#{self.table_name}.#{underlying_attr_name} = ?"],
598
+ [:"#{attr_name}__gte", "#{self.table_name}.#{underlying_attr_name} >= ?"],
599
+ [:"#{attr_name}__gt", "#{self.table_name}.#{underlying_attr_name} > ?"],
600
+ [:"#{attr_name}__lte", "#{self.table_name}.#{underlying_attr_name} <= ?"],
601
+ [:"#{attr_name}__lt", "#{self.table_name}.#{underlying_attr_name} < ?"],
602
+ ].each do |param_key, condition|
603
+ next if params[param_key].blank?
604
+ unless params[param_key].to_s.match?(/\A-?\d+\z/)
605
+ raise Eluvia::Errors::UnprocessableEntity.new({
606
+ "#{prefix}#{param_key}" => I18n.t('eluvia.errors.filter.invalid_integer_format',
607
+ default: 'Invalid integer format.')
608
+ })
609
+ end
610
+ scope = scope.where(condition, filters_model_class.clean_and_transform_value(params[param_key], attr_name))
611
+ end
612
+
613
+ end
614
+ scope
615
+ end
616
+
617
+ def apply_filters_for_date_attrs(scope, params, prefix: '')
618
+ return scope unless is_filters_model?(filters_model_class)
619
+ filters_model_class.attrs(type: :date).each do |attr_spec|
620
+ next if filters_model_class.has_filter_attr?(attr_spec[:attr_name])
621
+ attr_name = attr_spec[:attr_name]
622
+ underlying_attr_name = attr_spec[:underlying_attr_name] || attr_name
623
+
624
+ [
625
+ [attr_name, "#{self.table_name}.#{underlying_attr_name} = ?"],
626
+ [:"#{attr_name}__from", "#{self.table_name}.#{underlying_attr_name} >= ?"],
627
+ [:"#{attr_name}__to", "#{self.table_name}.#{underlying_attr_name} <= ?"],
628
+ ].each do |param_key, condition|
629
+ next if params[param_key].blank?
630
+ value = filters_model_class.clean_and_transform_value(params[param_key], attr_name)
631
+ msg = I18n.t('eluvia.errors.filter.invalid_date_format', default: 'Invalid date format.')
632
+ raise Eluvia::Errors::UnprocessableEntity.new({ "#{prefix}#{param_key}" => msg }) if value.nil?
633
+ scope = scope.where(condition, value)
634
+ end
635
+
636
+ end
637
+ scope
638
+ end
639
+
640
+ def apply_filters_for_boolean_attrs(scope, params)
641
+ return scope unless is_filters_model?(filters_model_class)
642
+ filters_model_class.attrs(type: :boolean).each do |attr_spec|
643
+ next if filters_model_class.has_filter_attr?(attr_spec[:attr_name])
644
+ attr_name = attr_spec[:attr_name]
645
+ underlying_attr_name = attr_spec[:underlying_attr_name] || attr_name
646
+
647
+ # Boolean match
648
+ unless params[attr_name].nil?
649
+ if filters_model_class.clean_value(params[attr_name], attr_name)
650
+ scope = scope.where("#{self.table_name}.#{underlying_attr_name} = ?", true)
651
+ else
652
+ scope = scope.where("#{self.table_name}.#{underlying_attr_name} IS NULL OR #{self.table_name}.#{attr_name} = ?", false)
653
+ end
654
+ end
655
+
656
+ end
657
+ scope
658
+ end
659
+
660
+ def _filters_model_class
661
+ filters_model_class_name = "#{self.name}::Filters"
662
+ filters_model_class_name.constantize
663
+ rescue NameError
664
+ nil
665
+ end
666
+
667
+ def filters_model_class
668
+ @filters_model_class ||= _filters_model_class
669
+ end
670
+
671
+ def is_filters_model?(model)
672
+ return false if model.nil?
673
+ return false unless model.respond_to?(:ancestors)
674
+ model.ancestors.include?(Eluvia::ActiveRecord::FiltersModel)
675
+ end
676
+
677
+ end
678
+ end
679
+ end
680
+ end