effective_datatables 1.4.3 → 1.5.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 +4 -4
- data/README.md +25 -2
- data/app/assets/images/dataTables/sort_asc.png +0 -0
- data/app/assets/images/dataTables/sort_both.png +0 -0
- data/app/assets/images/dataTables/sort_desc.png +0 -0
- data/app/assets/javascripts/dataTables/bootstrap/2/jquery.dataTables.bootstrap.js +148 -0
- data/app/assets/javascripts/dataTables/bootstrap/3/jquery.dataTables.bootstrap.js +185 -0
- data/app/assets/javascripts/dataTables/dataTables.colReorder.min.js +26 -0
- data/app/assets/javascripts/dataTables/dataTables.colVis.min.js +24 -0
- data/app/assets/javascripts/dataTables/dataTables.fixedColumns.min.js +30 -0
- data/app/assets/javascripts/dataTables/dataTables.tableTools.min.js +70 -0
- data/app/assets/javascripts/dataTables/jquery.dataTables.min.js +160 -0
- data/app/assets/javascripts/effective_datatables.bootstrap2.js +6 -5
- data/app/assets/javascripts/effective_datatables.js +6 -5
- data/app/assets/javascripts/effective_datatables/initialize.js.coffee.erb +52 -39
- data/app/assets/javascripts/vendor/jquery.debounce.min.js +9 -0
- data/app/assets/stylesheets/dataTables/bootstrap/2/jquery.dataTables.bootstrap.scss +207 -0
- data/app/assets/stylesheets/dataTables/bootstrap/3/jquery.dataTables.bootstrap.scss +280 -0
- data/app/assets/stylesheets/dataTables/dataTables.colReorder.min.css +1 -0
- data/app/assets/stylesheets/dataTables/dataTables.colVis.min.css +1 -0
- data/app/assets/stylesheets/dataTables/dataTables.fixedColumns.min.css +1 -0
- data/app/assets/stylesheets/dataTables/dataTables.tableTools.min.css +1 -0
- data/app/assets/stylesheets/dataTables/jquery.dataTables.min.css +1 -0
- data/app/assets/stylesheets/effective_datatables.bootstrap2.scss +4 -3
- data/app/assets/stylesheets/effective_datatables.scss +4 -3
- data/app/assets/stylesheets/effective_datatables/_overrides.scss.erb +71 -63
- data/app/controllers/effective/datatables_controller.rb +4 -4
- data/app/helpers/effective_datatables_helper.rb +24 -30
- data/app/models/effective/active_record_datatable_tool.rb +6 -6
- data/app/models/effective/array_datatable_tool.rb +18 -8
- data/app/models/effective/datatable.rb +98 -44
- data/app/views/effective/datatables/_datatable.html.haml +41 -15
- data/lib/effective_datatables.rb +0 -1
- data/lib/effective_datatables/version.rb +1 -1
- metadata +20 -17
- data/app/assets/javascripts/vendor/jquery.dataTables.columnFilter.js +0 -832
@@ -25,10 +25,10 @@ module Effective
|
|
25
25
|
|
26
26
|
def error_json
|
27
27
|
{
|
28
|
-
:
|
29
|
-
:
|
30
|
-
:
|
31
|
-
:
|
28
|
+
:draw => params[:draw].to_i,
|
29
|
+
:data => [],
|
30
|
+
:recordsTotal => 0,
|
31
|
+
:recordsFiltered => 0,
|
32
32
|
}.to_json
|
33
33
|
end
|
34
34
|
|
@@ -1,50 +1,40 @@
|
|
1
1
|
module EffectiveDatatablesHelper
|
2
2
|
def render_datatable(datatable, opts = {}, &block)
|
3
3
|
datatable.view = self
|
4
|
+
locals = {:style => :full, :filterable => true, :sortable => true, :table_class => 'table-bordered table-striped'}.merge(opts)
|
4
5
|
|
5
|
-
|
6
|
-
locals = locals.merge(opts) if opts.kind_of?(Hash)
|
7
|
-
locals[:table_class] = 'sorting-hidden ' + locals[:table_class].to_s if locals[:sortable] == false
|
8
|
-
|
9
|
-
# Do we have to look at empty? behaviour
|
10
|
-
if (block_given? || opts.kind_of?(String) || (opts.kind_of?(Hash) && opts[:empty].present?)) && datatable.empty?
|
11
|
-
if block_given?
|
12
|
-
yield; nil
|
13
|
-
elsif opts.kind_of?(String)
|
14
|
-
opts
|
15
|
-
elsif opts.kind_of?(Hash) && opts[:empty].present?
|
16
|
-
opts[:empty]
|
17
|
-
end
|
18
|
-
else
|
19
|
-
render :partial => 'effective/datatables/datatable', :locals => locals.merge(:datatable => datatable)
|
20
|
-
end
|
6
|
+
render :partial => 'effective/datatables/datatable', :locals => locals.merge(:datatable => datatable)
|
21
7
|
end
|
22
8
|
|
23
9
|
def render_simple_datatable(datatable, opts = {})
|
24
10
|
datatable.view = self
|
25
|
-
|
26
|
-
locals
|
11
|
+
datatable.per_page = :all
|
12
|
+
locals = {:style => :simple, :filterable => false, :sortable => false, :table_class => 'table-bordered table-striped sorting-hidden'}.merge(opts)
|
27
13
|
|
28
14
|
render :partial => 'effective/datatables/datatable', :locals => locals.merge(:datatable => datatable)
|
29
15
|
end
|
30
16
|
|
31
|
-
def
|
32
|
-
return
|
33
|
-
|
34
|
-
filters = datatable.table_columns.values.map { |options, _| options[:filter] || {:type => 'null'} }
|
17
|
+
def render_datatable_header_cell(form, name, opts, filterable = true)
|
18
|
+
return content_tag(:p, opts[:label] || name) if filterable == false
|
35
19
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
20
|
+
case opts[:filter][:type]
|
21
|
+
when :string, :text, :number
|
22
|
+
form.input name, :label => false, :required => false, :as => :string, :placeholder => (opts[:label] || name),
|
23
|
+
:input_html => { :autocomplete => 'off', :data => {'column-name' => opts[:name], 'column-index' => opts[:index]} }
|
24
|
+
when :select, :boolean
|
25
|
+
if opts[:filter][:values].respond_to?(:call)
|
26
|
+
opts[:filter][:values] = opts[:filter][:values].call()
|
40
27
|
|
41
|
-
if filter[:values].kind_of?(ActiveRecord::Relation) || (filter[:values].kind_of?(Array) && filter[:values].first.kind_of?(ActiveRecord::Base))
|
42
|
-
filter[:values] = filter[:values].map { |obj| [obj.
|
28
|
+
if opts[:filter][:values].kind_of?(ActiveRecord::Relation) || (opts[:filter][:values].kind_of?(Array) && opts[:filter][:values].first.kind_of?(ActiveRecord::Base))
|
29
|
+
opts[:filter][:values] = opts[:filter][:values].map { |obj| [obj.to_s, obj.id] }
|
43
30
|
end
|
44
31
|
end
|
45
|
-
end
|
46
32
|
|
47
|
-
|
33
|
+
form.input name, :label => false, :required => false, :as => :select, :collection => opts[:filter][:values], :include_blank => (opts[:label] || name.titleize),
|
34
|
+
:input_html => { :autocomplete => 'off', :data => {'column-name' => opts[:name], 'column-index' => opts[:index]} }
|
35
|
+
else
|
36
|
+
content_tag(:p, opts[:label] || name)
|
37
|
+
end
|
48
38
|
end
|
49
39
|
|
50
40
|
def datatable_non_sortable(datatable, sortable = true)
|
@@ -83,6 +73,10 @@ module EffectiveDatatablesHelper
|
|
83
73
|
end.to_json()
|
84
74
|
end
|
85
75
|
|
76
|
+
def datatable_column_names(datatable)
|
77
|
+
datatable.table_columns.values.map { |options| {:name => options[:name], :targets => options[:index] } }.to_json()
|
78
|
+
end
|
79
|
+
|
86
80
|
def datatables_admin_path?
|
87
81
|
@datatables_admin_path ||= (
|
88
82
|
path = request.path.to_s.downcase.chomp('/') + '/'
|
@@ -2,25 +2,25 @@ module Effective
|
|
2
2
|
class ActiveRecordDatatableTool
|
3
3
|
attr_accessor :table_columns
|
4
4
|
|
5
|
-
delegate :
|
5
|
+
delegate :order_name, :order_direction, :page, :per_page, :search_column, :to => :@datatable
|
6
6
|
|
7
7
|
def initialize(datatable, table_columns)
|
8
8
|
@datatable = datatable
|
9
9
|
@table_columns = table_columns
|
10
10
|
end
|
11
11
|
|
12
|
-
def order_column
|
13
|
-
@order_column ||= table_columns.find { |_, values| values[:index] == order_column_index }.try(:second) # This pulls out the values
|
14
|
-
end
|
15
|
-
|
16
12
|
def search_terms
|
17
13
|
@search_terms ||= @datatable.search_terms.select { |name, search_term| table_columns.key?(name) }
|
18
14
|
end
|
19
15
|
|
16
|
+
def order_column
|
17
|
+
@order_column ||= table_columns[order_name]
|
18
|
+
end
|
19
|
+
|
20
20
|
def order(collection)
|
21
21
|
return collection if order_column.blank?
|
22
22
|
|
23
|
-
if [:string, :text].include?(order_column[:type])
|
23
|
+
if [:string, :text].include?(order_column[:type]) && order_column[:sql_as_column] != true
|
24
24
|
collection.order("COALESCE(#{order_column[:column]}, '') #{order_direction}")
|
25
25
|
else
|
26
26
|
collection.order("#{order_column[:column]} #{order_direction} NULLS #{order_direction == 'ASC' ? 'LAST' : 'FIRST'}")
|
@@ -3,27 +3,29 @@ module Effective
|
|
3
3
|
class ArrayDatatableTool
|
4
4
|
attr_accessor :table_columns
|
5
5
|
|
6
|
-
delegate :
|
6
|
+
delegate :order_name, :order_direction, :page, :per_page, :search_column, :display_table_columns, :to => :@datatable
|
7
7
|
|
8
8
|
def initialize(datatable, table_columns)
|
9
9
|
@datatable = datatable
|
10
10
|
@table_columns = table_columns
|
11
11
|
end
|
12
12
|
|
13
|
-
def order_column
|
14
|
-
@order_column ||= table_columns.find { |_, values| values[:index] == order_column_index }.try(:second) # This pulls out the values
|
15
|
-
end
|
16
|
-
|
17
13
|
def search_terms
|
18
14
|
@search_terms ||= @datatable.search_terms.select { |name, search_term| table_columns.key?(name) }
|
19
15
|
end
|
20
16
|
|
17
|
+
def order_column
|
18
|
+
@order_column ||= table_columns[order_name]
|
19
|
+
end
|
20
|
+
|
21
21
|
def order(collection)
|
22
22
|
if order_column.present?
|
23
|
+
index = display_index(order_column)
|
24
|
+
|
23
25
|
if order_direction == 'ASC'
|
24
|
-
collection.sort! { |x, y| x[
|
26
|
+
collection.sort! { |x, y| x[index] <=> y[index] }
|
25
27
|
else
|
26
|
-
collection.sort! { |x, y| y[
|
28
|
+
collection.sort! { |x, y| y[index] <=> x[index] }
|
27
29
|
end
|
28
30
|
end
|
29
31
|
|
@@ -41,9 +43,10 @@ module Effective
|
|
41
43
|
|
42
44
|
def search_column_with_defaults(collection, table_column, search_term)
|
43
45
|
search_term = search_term.downcase
|
46
|
+
index = display_index(table_column)
|
44
47
|
|
45
48
|
collection.select! do |row|
|
46
|
-
value = row[
|
49
|
+
value = row[index].to_s.downcase
|
47
50
|
|
48
51
|
if table_column[:filter][:type] == :select && table_column[:filter][:fuzzy] != true
|
49
52
|
value == search_term
|
@@ -56,6 +59,13 @@ module Effective
|
|
56
59
|
def paginate(collection)
|
57
60
|
Kaminari.paginate_array(collection).page(page).per(per_page)
|
58
61
|
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def display_index(column)
|
66
|
+
(display_table_columns || table_columns).keys.index(column[:name])
|
67
|
+
end
|
68
|
+
|
59
69
|
end
|
60
70
|
end
|
61
71
|
|
@@ -22,6 +22,7 @@ module Effective
|
|
22
22
|
end
|
23
23
|
raise "You cannot use both :partial => '' and proc => ..." if options[:partial] && options[:proc]
|
24
24
|
|
25
|
+
send(:attr_accessor, name)
|
25
26
|
(@table_columns ||= HashWithIndifferentAccess.new())[name] = options
|
26
27
|
end
|
27
28
|
|
@@ -44,17 +45,25 @@ module Effective
|
|
44
45
|
def default_entries(entries)
|
45
46
|
@default_entries = entries
|
46
47
|
end
|
48
|
+
|
49
|
+
def model_name # Searching & Filters
|
50
|
+
@model_name ||= ActiveModel::Name.new(self)
|
51
|
+
end
|
47
52
|
end
|
48
53
|
|
54
|
+
|
49
55
|
def initialize(*args)
|
56
|
+
unless active_record_collection? || (collection.kind_of?(Array) && collection.first.kind_of?(Array))
|
57
|
+
raise "Unsupported collection type. Should be ActiveRecord class, ActiveRecord relation, or an Array of Arrays [[1, 'something'], [2, 'something else']]"
|
58
|
+
end
|
59
|
+
|
50
60
|
if args.present?
|
51
61
|
raise 'Effective::Datatable.new() can only be called with a Hash like arguments' unless args.first.kind_of?(Hash)
|
52
62
|
args.first.each { |k, v| self.attributes[k] = v }
|
53
63
|
end
|
54
64
|
|
55
|
-
|
56
|
-
|
57
|
-
end
|
65
|
+
# Any pre-selected search terms should be assigned now
|
66
|
+
search_terms.each { |column, term| self.send("#{column}=", term) }
|
58
67
|
end
|
59
68
|
|
60
69
|
# Any attributes set on initialize will be echoed back and available to the class
|
@@ -62,6 +71,13 @@ module Effective
|
|
62
71
|
@attributes ||= HashWithIndifferentAccess.new()
|
63
72
|
end
|
64
73
|
|
74
|
+
def to_key; []; end # Searching & Filters
|
75
|
+
|
76
|
+
# Instance method. In Rails 4.2 this needs to be defined on the instance, before it was on the class
|
77
|
+
def model_name # Searching & Filters
|
78
|
+
@model_name ||= ActiveModel::Name.new(self.class)
|
79
|
+
end
|
80
|
+
|
65
81
|
def to_param
|
66
82
|
self.class.name.underscore.parameterize
|
67
83
|
end
|
@@ -85,14 +101,27 @@ module Effective
|
|
85
101
|
end.each_with_index { |(_, col), index| col[:index] = index }
|
86
102
|
end
|
87
103
|
|
104
|
+
# This is for the ColReorder plugin
|
105
|
+
# It sends us a list of columns that are different than our table_columns order
|
106
|
+
# So this method just returns an array of column names, as per ColReorder
|
107
|
+
def display_table_columns
|
108
|
+
if params[:columns].present?
|
109
|
+
HashWithIndifferentAccess.new().tap do |display_columns|
|
110
|
+
params[:columns].each do |_, values|
|
111
|
+
display_columns[values[:name]] = table_columns[values[:name]]
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
88
117
|
def to_json
|
89
118
|
raise 'Effective::Datatable to_json called with a nil view. Please call render_datatable(@datatable) or @datatable.view = view before this method' unless view.present?
|
90
119
|
|
91
120
|
@json ||= {
|
92
|
-
:
|
93
|
-
:
|
94
|
-
:
|
95
|
-
:
|
121
|
+
:draw => (params[:draw] || 0),
|
122
|
+
:data => (table_data || []),
|
123
|
+
:recordsTotal => (total_records || 0),
|
124
|
+
:recordsFiltered => (display_records || 0)
|
96
125
|
}
|
97
126
|
end
|
98
127
|
|
@@ -104,20 +133,20 @@ module Effective
|
|
104
133
|
total_records.to_i == 0
|
105
134
|
end
|
106
135
|
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
136
|
+
def order_name
|
137
|
+
@order_name ||= begin
|
138
|
+
if params[:order] && params[:columns]
|
139
|
+
order_column_index = (params[:order].first[1][:column] rescue '0')
|
140
|
+
(params[:columns][order_column_index] || {})[:name]
|
141
|
+
elsif default_order.present?
|
142
|
+
default_order.keys.first
|
143
|
+
end || table_columns.keys.first
|
115
144
|
end
|
116
145
|
end
|
117
146
|
|
118
147
|
def order_direction
|
119
|
-
if params[:
|
120
|
-
params[:
|
148
|
+
@order_direction ||= if params[:order].present?
|
149
|
+
params[:order].first[1][:dir] == 'desc' ? 'DESC' : 'ASC'
|
121
150
|
elsif default_order.present?
|
122
151
|
default_order.values.first.to_s.downcase == 'desc' ? 'DESC' : 'ASC'
|
123
152
|
else
|
@@ -139,18 +168,15 @@ module Effective
|
|
139
168
|
|
140
169
|
def search_terms
|
141
170
|
@search_terms ||= HashWithIndifferentAccess.new().tap do |terms|
|
142
|
-
if params[:
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
171
|
+
if params[:columns].present? # This is an AJAX request from the DataTable
|
172
|
+
(params[:columns] || {}).each do |_, column|
|
173
|
+
next if table_columns[column[:name]].blank? || (column[:search] || {})[:value].blank?
|
174
|
+
|
175
|
+
terms[column[:name]] = column[:search][:value]
|
147
176
|
end
|
148
|
-
else
|
149
|
-
# We are in the initial render and have to apply default search terms only
|
177
|
+
else # This is the initial render, and we have to apply default search terms only
|
150
178
|
table_columns.each do |name, values|
|
151
|
-
|
152
|
-
terms[name] = values[:filter][:selected]
|
153
|
-
end
|
179
|
+
terms[name] = values[:filter][:selected] if values[:filter][:selected].present?
|
154
180
|
end
|
155
181
|
end
|
156
182
|
end
|
@@ -166,7 +192,7 @@ module Effective
|
|
166
192
|
end
|
167
193
|
|
168
194
|
def per_page
|
169
|
-
length = (params[:
|
195
|
+
length = (params[:length].presence || default_entries).to_i
|
170
196
|
|
171
197
|
if length == -1
|
172
198
|
9999999
|
@@ -177,8 +203,17 @@ module Effective
|
|
177
203
|
end
|
178
204
|
end
|
179
205
|
|
206
|
+
def per_page=(length)
|
207
|
+
case length
|
208
|
+
when Integer
|
209
|
+
params[:length] = length
|
210
|
+
when :all
|
211
|
+
params[:length] = -1
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
180
215
|
def page
|
181
|
-
params[:
|
216
|
+
params[:start].to_i / per_page + 1
|
182
217
|
end
|
183
218
|
|
184
219
|
def total_records
|
@@ -206,8 +241,14 @@ module Effective
|
|
206
241
|
(self.class.instance_methods(false) - [:collection, :search_column]).each do |view_method|
|
207
242
|
@view.class_eval { delegate view_method, :to => :@effective_datatable }
|
208
243
|
end
|
244
|
+
|
245
|
+
# Clear the search_terms memoization
|
246
|
+
@search_terms = nil
|
247
|
+
@order_name = nil
|
248
|
+
@order_direction = nil
|
209
249
|
end
|
210
250
|
|
251
|
+
|
211
252
|
protected
|
212
253
|
|
213
254
|
# So the idea here is that we want to do as much as possible on the database in ActiveRecord
|
@@ -267,7 +308,7 @@ module Effective
|
|
267
308
|
end
|
268
309
|
|
269
310
|
collection.each_with_index.map do |obj, index|
|
270
|
-
table_columns.map do |name, opts|
|
311
|
+
(display_table_columns || table_columns).map do |name, opts|
|
271
312
|
value = if opts[:partial]
|
272
313
|
rendered[name][index]
|
273
314
|
elsif opts[:block]
|
@@ -314,7 +355,11 @@ module Effective
|
|
314
355
|
end
|
315
356
|
|
316
357
|
def active_record_collection?
|
317
|
-
@active_record_collection
|
358
|
+
if @active_record_collection.nil?
|
359
|
+
@active_record_collection = (collection.ancestors.include?(ActiveRecord::Base) rescue false)
|
360
|
+
else
|
361
|
+
@active_record_collection
|
362
|
+
end
|
318
363
|
end
|
319
364
|
|
320
365
|
def table_columns_with_defaults
|
@@ -334,7 +379,7 @@ module Effective
|
|
334
379
|
belong_tos = (collection.ancestors.first.reflect_on_all_associations(:belongs_to) rescue []).inject(HashWithIndifferentAccess.new()) do |retval, bt|
|
335
380
|
unless bt.options[:polymorphic]
|
336
381
|
begin
|
337
|
-
klass = bt.klass || bt.foreign_type.
|
382
|
+
klass = bt.klass || bt.foreign_type.sub('_type', '').classify.constantize
|
338
383
|
rescue => e
|
339
384
|
klass = nil
|
340
385
|
end
|
@@ -370,6 +415,10 @@ module Effective
|
|
370
415
|
cols[name][:type] = :obfuscated_id
|
371
416
|
end
|
372
417
|
|
418
|
+
if sql_table.present? && sql_column.blank? # This is a SELECT AS column
|
419
|
+
cols[name][:sql_as_column] = true
|
420
|
+
end
|
421
|
+
|
373
422
|
cols[name][:filter] = initialize_table_column_filter(cols[name][:filter], cols[name][:type], belong_tos[name])
|
374
423
|
|
375
424
|
if cols[name][:partial]
|
@@ -379,33 +428,38 @@ module Effective
|
|
379
428
|
end
|
380
429
|
|
381
430
|
def initialize_table_column_filter(filter, col_type, belongs_to)
|
382
|
-
return {:type => :null
|
431
|
+
return {:type => :null} if filter == false
|
383
432
|
|
384
433
|
if filter.kind_of?(Symbol)
|
385
|
-
filter =
|
434
|
+
filter = HashWithIndifferentAccess.new(:type => filter)
|
386
435
|
elsif filter.kind_of?(String)
|
387
|
-
filter =
|
436
|
+
filter = HashWithIndifferentAccess.new(:type => filter.to_sym)
|
388
437
|
elsif filter.kind_of?(Hash) == false
|
389
|
-
filter =
|
438
|
+
filter = HashWithIndifferentAccess.new()
|
390
439
|
end
|
391
440
|
|
392
441
|
# This is a fix for passing filter[:selected] == false, it needs to be 'false'
|
393
442
|
filter[:selected] = filter[:selected].to_s unless filter[:selected].nil?
|
394
443
|
|
395
|
-
case col_type
|
444
|
+
filter = case col_type
|
396
445
|
when :belongs_to
|
397
|
-
|
446
|
+
HashWithIndifferentAccess.new(
|
398
447
|
:type => :select,
|
399
|
-
:
|
400
|
-
|
401
|
-
}.merge(filter)
|
448
|
+
:values => Proc.new { belongs_to[:klass].all.map { |obj| [obj.to_s, obj.id] }.sort { |x, y| x[1] <=> y[1] } }
|
449
|
+
).merge(filter)
|
402
450
|
when :integer
|
403
|
-
|
451
|
+
HashWithIndifferentAccess.new(:type => :number).merge(filter)
|
404
452
|
when :boolean
|
405
|
-
|
453
|
+
HashWithIndifferentAccess.new(:type => :boolean, :values => [true, false]).merge(filter)
|
406
454
|
else
|
407
|
-
|
455
|
+
HashWithIndifferentAccess.new(:type => :string).merge(filter)
|
456
|
+
end
|
457
|
+
|
458
|
+
if filter[:type] == :boolean
|
459
|
+
filter = HashWithIndifferentAccess.new(:values => [true, false]).merge(filter)
|
408
460
|
end
|
461
|
+
|
462
|
+
filter
|
409
463
|
end
|
410
464
|
|
411
465
|
end
|