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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +225 -0
- data/lib/eluvia/active_model/amount_attribute.rb +108 -0
- data/lib/eluvia/active_model/array_attribute.rb +134 -0
- data/lib/eluvia/active_model/attributes.rb +417 -0
- data/lib/eluvia/active_model/enum_array_attribute.rb +60 -0
- data/lib/eluvia/active_model/enum_attribute.rb +174 -0
- data/lib/eluvia/active_model/object_attribute.rb +143 -0
- data/lib/eluvia/active_model/range_attribute.rb +74 -0
- data/lib/eluvia/active_record/file_array_attribute.rb +116 -0
- data/lib/eluvia/active_record/file_attribute.rb +158 -0
- data/lib/eluvia/active_record/filtering.rb +680 -0
- data/lib/eluvia/active_record/filters_model.rb +58 -0
- data/lib/eluvia/active_record/ordering.rb +110 -0
- data/lib/eluvia/base/config.rb +38 -0
- data/lib/eluvia/base/version.rb +12 -0
- data/lib/eluvia/errors/bad_request.rb +17 -0
- data/lib/eluvia/errors/forbidden.rb +17 -0
- data/lib/eluvia/errors/not_found.rb +17 -0
- data/lib/eluvia/errors/service_unavailable.rb +17 -0
- data/lib/eluvia/errors/standard_error.rb +33 -0
- data/lib/eluvia/errors/unauthorized.rb +17 -0
- data/lib/eluvia/errors/unprocessable_entity.rb +26 -0
- data/lib/eluvia/fieldset/rest_field.rb +83 -0
- data/lib/eluvia/fieldset/rest_fieldset.rb +158 -0
- data/lib/eluvia/fieldset.rb +12 -0
- data/lib/eluvia/handlers/error_handler.rb +60 -0
- data/lib/eluvia/handlers/pagination_handler.rb +25 -0
- data/lib/eluvia/handlers/params_handler.rb +43 -0
- data/lib/eluvia/helpers/attachment_helper.rb +37 -0
- data/lib/eluvia/integrations/eluvia_integration.rb +281 -0
- data/lib/eluvia/models/file.rb +44 -0
- data/lib/eluvia/models/file_wrapper.rb +38 -0
- data/lib/eluvia/models/image_wrapper.rb +38 -0
- data/lib/eluvia/serializers/error_serializer.rb +21 -0
- data/lib/eluvia/serializers/fieldset_serializer.rb +63 -0
- data/lib/eluvia/services/health_service.rb +56 -0
- data/lib/eluvia/uploads.rb +13 -0
- data/lib/eluvia/utils/hash_and_array.rb +98 -0
- data/lib/eluvia/utils/jwt.rb +36 -0
- data/lib/eluvia/utils/string.rb +31 -0
- data/lib/eluvia/utils/uuid.rb +21 -0
- data/lib/eluvia-base.rb +79 -0
- 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
|