miscellany 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +3 -0
- data/config/initializers/01_custom_preloaders.rb +2 -0
- data/config/initializers/arbitrary_prefetch.rb +1 -0
- data/config/initializers/cancancan.rb +34 -0
- data/lib/miscellany.rb +6 -0
- data/lib/miscellany/active_record/arbitrary_prefetch.rb +163 -0
- data/lib/miscellany/active_record/batch_matcher.rb +199 -0
- data/lib/miscellany/active_record/batched_destruction.rb +96 -0
- data/lib/miscellany/active_record/custom_preloaders.rb +59 -0
- data/lib/miscellany/batch_processor.rb +41 -0
- data/lib/miscellany/batching_csv_processor.rb +86 -0
- data/lib/miscellany/controller/http_error_handling.rb +62 -0
- data/lib/miscellany/controller/json_uploads.rb +41 -0
- data/lib/miscellany/controller/sliced_response.rb +271 -0
- data/lib/miscellany/param_validator.rb +442 -0
- data/lib/miscellany/version.rb +3 -0
- data/miscellany.gemspec +43 -0
- metadata +297 -0
@@ -0,0 +1,59 @@
|
|
1
|
+
# Example Usage (Preloading items with reference to the same Polymorphic Object):
|
2
|
+
#
|
3
|
+
# has_many :related_objects, -> (self) { where(poly_type: self.poly_type, poly_id: self.poly_id) }, preloader: 'RelatedObjectPreloader'
|
4
|
+
#
|
5
|
+
# class RelatedObjectPreloader < ActiveRecord::Associations::Preloader::Association
|
6
|
+
# def run(preloader)
|
7
|
+
# @preloaded_records = []
|
8
|
+
# owners.group_by(&:poly_type).each do |type, owner_group|
|
9
|
+
# ids = owner_group.map(&:poly_id)
|
10
|
+
#
|
11
|
+
# ids_to_priors = {}
|
12
|
+
# scope = Poly.scope_for_association.where(poly_type: self.poly_type, poly_id: self.poly_id)
|
13
|
+
# scope.find_each do |pa|
|
14
|
+
# ids_to_priors[pa.poly_id] ||= []
|
15
|
+
# ids_to_priors[pa.poly_id] << pa
|
16
|
+
# @preloaded_records << pa
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# owner_group.each do |owner|
|
20
|
+
# priors = ids_to_priors[owner.poly_id] || []
|
21
|
+
#
|
22
|
+
# association = owner.association(reflection.name)
|
23
|
+
# association.loaded!
|
24
|
+
# association.target = priors
|
25
|
+
#
|
26
|
+
# # association.set_inverse_instance(record)
|
27
|
+
# end
|
28
|
+
# end
|
29
|
+
# end
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
module Miscellany
|
33
|
+
module CustomPreloaders
|
34
|
+
module AssociationBuilderExtension
|
35
|
+
def self.build(model, reflection); end
|
36
|
+
|
37
|
+
def self.valid_options
|
38
|
+
[:preloader]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
module PreloaderExtension
|
43
|
+
def preloader_for(reflection, owners)
|
44
|
+
cust_preloader = reflection.options[:preloader]
|
45
|
+
if cust_preloader.present?
|
46
|
+
cust_preloader = cust_preloader.constantize if cust_preloader.is_a?(String)
|
47
|
+
cust_preloader
|
48
|
+
else
|
49
|
+
super
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.install
|
55
|
+
ActiveRecord::Associations::Builder::Association.extensions << AssociationBuilderExtension
|
56
|
+
ActiveRecord::Associations::Preloader.prepend(CustomPreloaders::PreloaderExtension)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Miscellany
|
2
|
+
# An array that "processes" after so many items are added.
|
3
|
+
#
|
4
|
+
# Example Usage:
|
5
|
+
# batches = BatchProcessor.new(of: 1000) do |batch|
|
6
|
+
# # Process the batch somehow
|
7
|
+
# end
|
8
|
+
# enumerator_of_some_kind.each { |item| batches << item }
|
9
|
+
# batches.flush
|
10
|
+
class BatchProcessor
|
11
|
+
attr_reader :batch_size
|
12
|
+
|
13
|
+
def initialize(of: 1000, &blk)
|
14
|
+
@batch_size = of
|
15
|
+
@block = blk
|
16
|
+
@current_batch = []
|
17
|
+
end
|
18
|
+
|
19
|
+
def <<(item)
|
20
|
+
@current_batch << item
|
21
|
+
process_batch if @current_batch.count >= batch_size
|
22
|
+
end
|
23
|
+
|
24
|
+
def add_all(items)
|
25
|
+
items.each do |i|
|
26
|
+
self << i
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def flush
|
31
|
+
process_batch if @current_batch.present?
|
32
|
+
end
|
33
|
+
|
34
|
+
protected
|
35
|
+
|
36
|
+
def process_batch
|
37
|
+
@block.call(@current_batch)
|
38
|
+
@current_batch = []
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module Miscellany
|
2
|
+
class BatchingCsvProcessor
|
3
|
+
attr_accessor :csv, :file_name
|
4
|
+
|
5
|
+
class RowError < StandardError; end
|
6
|
+
|
7
|
+
def initialize(csv, file_name: nil)
|
8
|
+
@csv = csv
|
9
|
+
@file_name = file_name
|
10
|
+
end
|
11
|
+
|
12
|
+
def process_in_batches(&blk)
|
13
|
+
batch = BatchProcessor.new(&blk)
|
14
|
+
CSV.new(csv, headers: true, header_converters: :symbol).each.with_index do |row, line|
|
15
|
+
row[:line_number] = line + 1
|
16
|
+
next unless validate_line(row)
|
17
|
+
|
18
|
+
batch << row
|
19
|
+
end
|
20
|
+
batch.flush
|
21
|
+
end
|
22
|
+
|
23
|
+
def get_row_errors(row); end
|
24
|
+
|
25
|
+
def find_or_init(_row)
|
26
|
+
raise NotImplementedError
|
27
|
+
end
|
28
|
+
|
29
|
+
def apply_row_to_model(_row, _instance)
|
30
|
+
raise NotImplementedError
|
31
|
+
end
|
32
|
+
|
33
|
+
def log_line_error(message, line_number, **kwargs)
|
34
|
+
raise NotImplementedError
|
35
|
+
end
|
36
|
+
|
37
|
+
def validate_line(row)
|
38
|
+
errors = get_row_errors(row) || []
|
39
|
+
if errors.present?
|
40
|
+
log_line_error(errors[0], row[:line_number])
|
41
|
+
false
|
42
|
+
else
|
43
|
+
true
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def batch_rows_to_models(rows)
|
48
|
+
rows.map { |row| build_model_from_row(row) }.reject! { |inst| inst.nil? || !inst.changed? }
|
49
|
+
end
|
50
|
+
|
51
|
+
def build_model_from_row(row)
|
52
|
+
model = find_or_init(row)
|
53
|
+
apply_row_to_model(row, model)
|
54
|
+
return nil if model.respond_to?(:discarded_at) && model.discarded_at.present? && !model.persisted?
|
55
|
+
|
56
|
+
model
|
57
|
+
rescue ActiveRecord::RecordNotFound, RowError => err
|
58
|
+
log_line_error(err.message, row[:line_number], exception: err)
|
59
|
+
nil
|
60
|
+
rescue StandardError => err
|
61
|
+
log_line_error('An Internal Error Occurred', row[:line_number], exception: err)
|
62
|
+
Raven.capture_exception(err)
|
63
|
+
nil
|
64
|
+
end
|
65
|
+
|
66
|
+
def map_defined_columns(row, map)
|
67
|
+
newh = {}
|
68
|
+
map.each do |newk, oldk|
|
69
|
+
next unless row.include?(oldk)
|
70
|
+
|
71
|
+
newh[newk] = row[oldk]
|
72
|
+
end
|
73
|
+
newh
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.file_matches?(file)
|
77
|
+
header = file.readline.strip.split(',')
|
78
|
+
file.try(:rewind)
|
79
|
+
headers_match?(header)
|
80
|
+
end
|
81
|
+
|
82
|
+
def self.headers_match?(headers)
|
83
|
+
(self::HEADERS - headers).empty?
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module Miscellany
|
2
|
+
module HttpErrorHandling
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
class UnauthorizedError < StandardError; end
|
6
|
+
|
7
|
+
class HttpError < StandardError
|
8
|
+
attr_accessor :status, :extra
|
9
|
+
def initialize(arg = nil, status: nil, message: '', **extra)
|
10
|
+
if arg.is_a?(Numeric)
|
11
|
+
raise ArgumentError, ':status supplied multiple times' if status.present?
|
12
|
+
|
13
|
+
status = arg
|
14
|
+
elsif arg.present?
|
15
|
+
raise ArgumentError, ':message supplied multiple times' if message.present?
|
16
|
+
|
17
|
+
message = arg
|
18
|
+
end
|
19
|
+
super(message)
|
20
|
+
@status = status
|
21
|
+
@extra = extra
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
module ClassMethods
|
26
|
+
def http_error(status = 400, message = nil, &blk)
|
27
|
+
message = blk if blk.present?
|
28
|
+
lambda { |err|
|
29
|
+
# This is necessary to ensure that HttpErrors are never handled as StandardErrors
|
30
|
+
if err.is_a?(HttpError)
|
31
|
+
render_http_error(err)
|
32
|
+
else
|
33
|
+
render_http_error(err, status: status, message: message)
|
34
|
+
end
|
35
|
+
}
|
36
|
+
end
|
37
|
+
|
38
|
+
def rescue_with_http_error(*errors, status: nil, message: nil, **kwargs)
|
39
|
+
rescue_from(*errors, with: http_error(status, message), **kwargs)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
included do
|
44
|
+
rescue_from HttpError do |err|
|
45
|
+
render_http_error(err)
|
46
|
+
end
|
47
|
+
|
48
|
+
rescue_with_http_error UnauthorizedError, status: 401
|
49
|
+
end
|
50
|
+
|
51
|
+
def render_http_error(err, status: nil, message: nil)
|
52
|
+
status = err.status if err.is_a?(HttpError) && status.nil?
|
53
|
+
status ||= 400
|
54
|
+
message ||= err.message
|
55
|
+
message.message.call(err) if message.is_a?(Proc)
|
56
|
+
response_json = { status: status }
|
57
|
+
response_json[:message] = message if message.present?
|
58
|
+
response_json.merge!(err.extra)
|
59
|
+
render json: response_json, status: status
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Miscellany
|
2
|
+
module JsonUploads
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
# This hackery allows using JSON params along with file-uploads
|
6
|
+
# On the frontend, use MultiPart form data, and add _parameters as an entry
|
7
|
+
def params
|
8
|
+
if @_params.nil?
|
9
|
+
params_obj = @_params = super
|
10
|
+
phash = params_obj.instance_variable_get(:@parameters)
|
11
|
+
if phash[:_parameters].present?
|
12
|
+
json_layer = JSON.parse(phash.delete(:_parameters))
|
13
|
+
shared_keys = phash.keys & json_layer.keys
|
14
|
+
main_layer = phash.slice(*shared_keys)
|
15
|
+
merge_params(json_layer, main_layer)
|
16
|
+
phash.merge!(json_layer)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
@_params
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def merge_params(base, layer)
|
25
|
+
if base.is_a?(Array) && (layer.is_a?(Hash) || layer.is_a?(Array))
|
26
|
+
base.each_with_index.map do |v, i|
|
27
|
+
over_key = layer.is_a?(Hash) ? i.to_s : i
|
28
|
+
merge_params(v, layer[over_key])
|
29
|
+
end
|
30
|
+
elsif base.is_a?(Hash) && layer.is_a?(Hash)
|
31
|
+
base.each do |k, v|
|
32
|
+
base[k] = merge_params(v, layer[k])
|
33
|
+
end
|
34
|
+
layer.merge(base)
|
35
|
+
else
|
36
|
+
Rails.logger.warn 'Duplicate parameter passed' if base.present? && layer.present?
|
37
|
+
layer.presence || base
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,271 @@
|
|
1
|
+
module Miscellany
|
2
|
+
module SlicedResponse
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
include HttpErrorHandling
|
6
|
+
|
7
|
+
def slice_results(queryset, **kwargs)
|
8
|
+
@sliced_data = sliced_json(queryset, **kwargs) { |x| x }
|
9
|
+
end
|
10
|
+
|
11
|
+
# rubocop:disable Metrics/ParameterLists
|
12
|
+
def sliced_json(
|
13
|
+
queryset, slice_params = params,
|
14
|
+
max_size: 50, default_size: 25, allow_all: false,
|
15
|
+
default_sort: nil, valid_sorts: nil,
|
16
|
+
&blk
|
17
|
+
)
|
18
|
+
normalized_sorts = normalize_sort_options(valid_sorts || queryset.column_names, default: default_sort)
|
19
|
+
|
20
|
+
slice = Slice.build(
|
21
|
+
queryset, slice_params,
|
22
|
+
default_page_size: default_size,
|
23
|
+
item_transformer: blk,
|
24
|
+
max_size: max_size,
|
25
|
+
default_size: default_size,
|
26
|
+
allow_all: allow_all,
|
27
|
+
valid_sorts: normalized_sorts,
|
28
|
+
)
|
29
|
+
|
30
|
+
slice.render_json
|
31
|
+
end
|
32
|
+
# rubocop:enable Metrics/ParameterLists
|
33
|
+
|
34
|
+
def as_sliced_json(queryset, slice: nil, total_count: nil, &blk)
|
35
|
+
slice = Slice.build(queryset, slice, total_count: total_count, item_transformer: blk)
|
36
|
+
slice.render_json
|
37
|
+
end
|
38
|
+
|
39
|
+
def bearcat_as_sliced_json(*args, transform: nil, **kwargs, &blk)
|
40
|
+
bearcat_exec = ->(slice) {
|
41
|
+
response = blk.call({
|
42
|
+
per_page: slice.page_size,
|
43
|
+
page: slice.page_number,
|
44
|
+
})
|
45
|
+
|
46
|
+
slice.rendered_json[:page_count] = response.page_count
|
47
|
+
|
48
|
+
response
|
49
|
+
}
|
50
|
+
sliced_json(bearcat_exec, *args, valid_sorts: {}, **kwargs, allow_all: false, &transform)
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def normalize_sort_options(sorts, default: nil)
|
56
|
+
norm_sorts = { }
|
57
|
+
|
58
|
+
sorts.each do |s|
|
59
|
+
if s.is_a?(Hash)
|
60
|
+
s.each do |k,v|
|
61
|
+
sort_hash = normalize_sort(v, key: k)
|
62
|
+
norm_sorts[k] = sort_hash
|
63
|
+
end
|
64
|
+
else
|
65
|
+
sort_hash = normalize_sort(s)
|
66
|
+
norm_sorts[sort_hash[:column]] = sort_hash
|
67
|
+
# default ||= sort_hash
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
if default.present?
|
72
|
+
norm_default = normalize_sort(default)
|
73
|
+
reference = norm_sorts[norm_default[:key].to_s] || norm_default
|
74
|
+
norm_sorts[:default] = {
|
75
|
+
key: reference[:key],
|
76
|
+
column: reference[:column],
|
77
|
+
order: norm_default[:order] || reference[:order],
|
78
|
+
}
|
79
|
+
end
|
80
|
+
|
81
|
+
norm_sorts
|
82
|
+
end
|
83
|
+
|
84
|
+
def normalize_sort(sort, key: nil)
|
85
|
+
sort = sort.to_s if sort.is_a?(Symbol)
|
86
|
+
if sort.is_a?(String)
|
87
|
+
m = sort.match(/(\w+)(?: (ASC|DESC)(!?))?/)
|
88
|
+
sort = { column: m[1], order: m[2], force_order: m[3].present? }.compact
|
89
|
+
elsif sort.is_a?(Proc)
|
90
|
+
sort = { column: sort }
|
91
|
+
end
|
92
|
+
sort[:key] = key || sort[:column]
|
93
|
+
sort.compact
|
94
|
+
end
|
95
|
+
|
96
|
+
class Slice
|
97
|
+
attr_accessor :slice_start, :slice_end
|
98
|
+
attr_accessor :page_size, :page_number
|
99
|
+
attr_accessor :sort
|
100
|
+
|
101
|
+
attr_reader :items
|
102
|
+
attr_reader :options
|
103
|
+
|
104
|
+
def initialize(items, options={})
|
105
|
+
@items = items
|
106
|
+
@options = options
|
107
|
+
end
|
108
|
+
|
109
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
110
|
+
def self.build(queryset, arg, options={})
|
111
|
+
new(queryset, options).tap do |slice|
|
112
|
+
if arg.is_a?(Array)
|
113
|
+
slice_bounds = arg
|
114
|
+
elsif arg.is_a?(String)
|
115
|
+
slice_bounds = arg.split(':').map(&:to_i)
|
116
|
+
elsif arg.is_a?(Range)
|
117
|
+
slice_bounds = arg.minmax
|
118
|
+
elsif arg.respond_to? :[]
|
119
|
+
if arg[:slice] == 'all' || arg[:page] == 'all'
|
120
|
+
slice_bounds = [0, -1]
|
121
|
+
elsif arg[:slice].present?
|
122
|
+
slice_bounds = arg[:slice].split(':').map(&:to_i)
|
123
|
+
else
|
124
|
+
page_size = slice[:page_size] = (arg[:page_size] || options[:default_page_size]).to_i
|
125
|
+
page_number = slice[:page_number] = (arg[:page] || 1).to_i
|
126
|
+
slice_bounds = [(page_number - 1) * page_size, page_number * page_size]
|
127
|
+
end
|
128
|
+
|
129
|
+
slice[:sort] = parse_and_validate_sorts(arg[:sort], options[:valid_sorts])
|
130
|
+
end
|
131
|
+
|
132
|
+
if slice_bounds.present?
|
133
|
+
slice[:slice_start] = slice_bounds[0]
|
134
|
+
slice[:slice_end] = slice_bounds[1]
|
135
|
+
end
|
136
|
+
|
137
|
+
if slice[:slice_end] == -1
|
138
|
+
raise HttpErrorHandling::HttpError, message: "cannot request whole collection" unless options[:allow_all]
|
139
|
+
else
|
140
|
+
if options[:max_size] && (slice[:slice_end] - slice[:slice_start]) > [options[:max_size], options[:default_size]].max
|
141
|
+
raise HttpErrorHandling::HttpError, message: "cannot request more than #{options[:max_size]} objects"
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
147
|
+
|
148
|
+
def [](key)
|
149
|
+
send(key)
|
150
|
+
end
|
151
|
+
|
152
|
+
def []=(key, val)
|
153
|
+
send(:"#{key}=", val)
|
154
|
+
end
|
155
|
+
|
156
|
+
def render_json
|
157
|
+
return @rendered_json if defined?(@rendered_json)
|
158
|
+
|
159
|
+
json = @rendered_json = {
|
160
|
+
slice_start: slice_start,
|
161
|
+
slice_end: slice_end,
|
162
|
+
}
|
163
|
+
|
164
|
+
rendered_items
|
165
|
+
|
166
|
+
if page_size.present?
|
167
|
+
json[:page] ||= page_number
|
168
|
+
json[:page_size] ||= page_size
|
169
|
+
end
|
170
|
+
|
171
|
+
if total_item_count.present?
|
172
|
+
json[:total_count] ||= total_item_count
|
173
|
+
json[:page_count] ||= (total_item_count.to_f / page_size).ceil if page_size.present?
|
174
|
+
end
|
175
|
+
|
176
|
+
if sort.present?
|
177
|
+
json[:sort] ||= sort.map do |s|
|
178
|
+
"#{s[:key] || s[:column]} #{s[:order] || 'ASC'}"
|
179
|
+
end.join(', ')
|
180
|
+
end
|
181
|
+
|
182
|
+
json[:slice_start] ||= 0
|
183
|
+
json[:slice_end] ||= total_item_count
|
184
|
+
|
185
|
+
json[:items] = rendered_items
|
186
|
+
|
187
|
+
json
|
188
|
+
end
|
189
|
+
def rendered_json; render_json; end
|
190
|
+
|
191
|
+
def self.parse_and_validate_sorts(sortstr, sorts_map, silent_failure: true)
|
192
|
+
(sortstr || '').split(',').map do |s|
|
193
|
+
m = s.strip.match(/^(\w+)(?: (ASC|DESC))?$/)
|
194
|
+
|
195
|
+
if m.nil?
|
196
|
+
next if silent_failure
|
197
|
+
raise HttpErrorHandling::HttpError, message: 'Could not parse sort parameter'
|
198
|
+
end
|
199
|
+
|
200
|
+
resolved_sort = sorts_map[m[1]]
|
201
|
+
unless resolved_sort.present?
|
202
|
+
next if silent_failure
|
203
|
+
raise HttpErrorHandling::HttpError, message: 'Could not parse sort parameter'
|
204
|
+
end
|
205
|
+
|
206
|
+
sort = resolved_sort.dup
|
207
|
+
sort[:order] = m[2] if m[2].present? && !sort[:force_order]
|
208
|
+
sort
|
209
|
+
end.compact.presence || [sorts_map[:default]].compact
|
210
|
+
end
|
211
|
+
|
212
|
+
private
|
213
|
+
|
214
|
+
def rendered_items
|
215
|
+
ritems = sliced_items
|
216
|
+
ritems = ritems.map(&options[:item_transformer]) if options[:item_transformer]
|
217
|
+
ritems
|
218
|
+
end
|
219
|
+
|
220
|
+
def total_item_count
|
221
|
+
@total_item_count ||= options[:total_count] || (items.respond_to?(:count) && items.count) || nil
|
222
|
+
end
|
223
|
+
|
224
|
+
def sliced_items
|
225
|
+
@sliced_items ||= begin
|
226
|
+
if items.is_a?(Array)
|
227
|
+
items
|
228
|
+
elsif items.is_a?(Proc)
|
229
|
+
items.call(self)
|
230
|
+
elsif items.is_a?(ActiveRecord::Relation)
|
231
|
+
offset, limit = slice_bounds
|
232
|
+
limit -= offset unless limit.nil?
|
233
|
+
apply_ar_sort(items).limit(limit).offset(offset)
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
def slice_bounds
|
239
|
+
[slice_start, slice_end == -1 ? nil : slice_end]
|
240
|
+
end
|
241
|
+
|
242
|
+
def apply_ar_sort(qset)
|
243
|
+
if sort.present?
|
244
|
+
sorts = [ *Array(self.sort) ]
|
245
|
+
sorts << options[:valid_sorts][:default] if options.dig(:valid_sorts, :default).present?
|
246
|
+
|
247
|
+
sorts.reduce(qset) do |qset, sort|
|
248
|
+
if sort[:column].is_a?(Proc)
|
249
|
+
sort[:column].call(qset, sort[:order] || 'ASC')
|
250
|
+
else
|
251
|
+
qset.order(sort[:column] => sort[:order] || 'ASC')
|
252
|
+
end
|
253
|
+
end
|
254
|
+
else
|
255
|
+
qset
|
256
|
+
end
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
module JbuilderTemplateExt
|
261
|
+
def partial!(*args, **kwargs, &blk)
|
262
|
+
kwargs[:block] = blk if blk.present?
|
263
|
+
super(*args, **kwargs)
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
def self.install_extensions
|
268
|
+
::JbuilderTemplate.prepend JbuilderTemplateExt
|
269
|
+
end
|
270
|
+
end
|
271
|
+
end
|