effective_datatables 2.12.2 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +632 -512
  3. data/app/assets/javascripts/dataTables/buttons/buttons.html5.js +176 -177
  4. data/app/assets/javascripts/dataTables/buttons/buttons.print.js +2 -0
  5. data/app/assets/javascripts/dataTables/buttons/dataTables.buttons.js +14 -14
  6. data/app/assets/javascripts/dataTables/dataTables.bootstrap.js +1 -1
  7. data/app/assets/javascripts/dataTables/jquery.dataTables.js +246 -217
  8. data/app/assets/javascripts/effective_datatables.js +2 -3
  9. data/app/assets/javascripts/effective_datatables/events.js.coffee +7 -0
  10. data/app/assets/javascripts/effective_datatables/filters.js.coffee +6 -0
  11. data/app/assets/javascripts/effective_datatables/initialize.js.coffee +42 -39
  12. data/app/assets/javascripts/effective_datatables/reset.js.coffee +7 -0
  13. data/app/assets/javascripts/vendor/jquery.delayedChange.js +1 -1
  14. data/app/assets/stylesheets/dataTables/dataTables.bootstrap.css +0 -1
  15. data/app/assets/stylesheets/effective_datatables.scss +1 -2
  16. data/app/assets/stylesheets/effective_datatables/{_scopes.scss → _filters.scss} +1 -1
  17. data/app/assets/stylesheets/effective_datatables/_overrides.scss +1 -1
  18. data/app/controllers/effective/datatables_controller.rb +2 -4
  19. data/app/helpers/effective_datatables_helper.rb +56 -91
  20. data/app/helpers/effective_datatables_private_helper.rb +55 -64
  21. data/app/models/effective/datatable.rb +103 -177
  22. data/app/models/effective/datatable_column.rb +28 -0
  23. data/app/models/effective/datatable_column_tool.rb +110 -0
  24. data/app/models/effective/datatable_dsl_tool.rb +28 -0
  25. data/app/models/effective/datatable_value_tool.rb +142 -0
  26. data/app/models/effective/effective_datatable/attributes.rb +25 -0
  27. data/app/models/effective/effective_datatable/collection.rb +38 -0
  28. data/app/models/effective/effective_datatable/compute.rb +154 -0
  29. data/app/models/effective/effective_datatable/cookie.rb +29 -0
  30. data/app/models/effective/effective_datatable/dsl.rb +14 -8
  31. data/app/models/effective/effective_datatable/dsl/bulk_actions.rb +5 -6
  32. data/app/models/effective/effective_datatable/dsl/charts.rb +7 -9
  33. data/app/models/effective/effective_datatable/dsl/datatable.rb +107 -57
  34. data/app/models/effective/effective_datatable/dsl/filters.rb +50 -0
  35. data/app/models/effective/effective_datatable/format.rb +157 -0
  36. data/app/models/effective/effective_datatable/hooks.rb +0 -18
  37. data/app/models/effective/effective_datatable/params.rb +34 -0
  38. data/app/models/effective/effective_datatable/resource.rb +108 -0
  39. data/app/models/effective/effective_datatable/state.rb +178 -0
  40. data/app/views/effective/datatables/_actions_column.html.haml +9 -42
  41. data/app/views/effective/datatables/_bulk_actions_column.html.haml +1 -1
  42. data/app/views/effective/datatables/_bulk_actions_dropdown.html.haml +2 -3
  43. data/app/views/effective/datatables/_chart.html.haml +1 -1
  44. data/app/views/effective/datatables/_datatable.html.haml +7 -25
  45. data/app/views/effective/datatables/_filters.html.haml +21 -0
  46. data/app/views/effective/datatables/_reset.html.haml +2 -0
  47. data/app/views/effective/datatables/_resource_column.html.haml +8 -0
  48. data/app/views/effective/datatables/index.html.haml +0 -1
  49. data/config/effective_datatables.rb +9 -32
  50. data/lib/effective_datatables.rb +2 -6
  51. data/lib/effective_datatables/engine.rb +1 -1
  52. data/lib/effective_datatables/version.rb +1 -1
  53. data/lib/generators/effective_datatables/install_generator.rb +2 -2
  54. metadata +39 -19
  55. data/app/assets/javascripts/dataTables/colreorder/dataTables.colReorder.js +0 -27
  56. data/app/assets/javascripts/dataTables/jszip/jszip.js +0 -9155
  57. data/app/assets/javascripts/effective_datatables/scopes.js.coffee +0 -9
  58. data/app/models/effective/active_record_datatable_tool.rb +0 -242
  59. data/app/models/effective/array_datatable_tool.rb +0 -97
  60. data/app/models/effective/effective_datatable/ajax.rb +0 -101
  61. data/app/models/effective/effective_datatable/charts.rb +0 -20
  62. data/app/models/effective/effective_datatable/dsl/scopes.rb +0 -23
  63. data/app/models/effective/effective_datatable/helpers.rb +0 -24
  64. data/app/models/effective/effective_datatable/options.rb +0 -309
  65. data/app/models/effective/effective_datatable/rendering.rb +0 -365
  66. data/app/views/effective/datatables/_scopes.html.haml +0 -21
@@ -0,0 +1,28 @@
1
+ module Effective
2
+ class DatatableColumn
3
+ attr_accessor :attributes
4
+
5
+ delegate :[], :[]=, to: :attributes
6
+
7
+ def initialize(attributes)
8
+ @attributes = attributes
9
+ end
10
+
11
+ def to_s
12
+ self[:name]
13
+ end
14
+
15
+ def format(&block)
16
+ @attributes[:format] = block; self
17
+ end
18
+
19
+ def search(&block)
20
+ @attributes[:search_method] = block; self
21
+ end
22
+
23
+ def sort(&block)
24
+ @attributes[:sort_method] = block; self
25
+ end
26
+
27
+ end
28
+ end
@@ -0,0 +1,110 @@
1
+ module Effective
2
+ class DatatableColumnTool
3
+ attr_reader :datatable
4
+ attr_reader :columns
5
+
6
+ def initialize(datatable)
7
+ @datatable = datatable
8
+
9
+ if datatable.active_record_collection?
10
+ @columns = datatable.columns.select { |_, col| col[:sql_column].present? }
11
+ else
12
+ @columns = {}
13
+ end
14
+ end
15
+
16
+ def scoped
17
+ @scoped ||= datatable._scopes[datatable.scope]
18
+ end
19
+
20
+ def searched
21
+ @searched ||= datatable.search.select { |name, _| columns.key?(name) }
22
+ end
23
+
24
+ def ordered
25
+ @ordered_column ||= columns[datatable.order_name]
26
+ end
27
+
28
+ def order(collection)
29
+ return collection unless ordered.present?
30
+
31
+ collection = if ordered[:sort_method]
32
+ datatable.dsl_tool.instance_exec(collection, datatable.order_direction, ordered, ordered[:sql_column], &ordered[:sort_method])
33
+ else
34
+ order_column(collection, datatable.order_direction, ordered, ordered[:sql_column])
35
+ end
36
+
37
+ raise 'sort method must return an ActiveRecord::Relation object' unless collection.kind_of?(ActiveRecord::Relation)
38
+
39
+ collection
40
+ end
41
+
42
+ def order_column(collection, direction, column, sql_column)
43
+ Rails.logger.info "COLUMN TOOL: order_column #{column.to_s} #{direction} #{sql_column}"
44
+
45
+ if column[:sql_as_column]
46
+ collection.order("#{sql_column} #{datatable.resource.sql_direction(direction)}")
47
+ else
48
+ Effective::Resource.new(collection)
49
+ .order(column[:name], direction, as: column[:as], sort: column[:sort], sql_column: column[:sql_column])
50
+ end
51
+ end
52
+
53
+ def scope(collection)
54
+ return collection unless scoped.present?
55
+
56
+ collection.send(scoped[:name], *scoped[:args])
57
+ end
58
+
59
+ def search(collection)
60
+ searched.each do |name, value|
61
+ column = columns[name]
62
+
63
+ collection = if column[:search_method]
64
+ datatable.dsl_tool.instance_exec(collection, value, column, column[:sql_column], &column[:search_method])
65
+ else
66
+ search_column(collection, value, column, column[:sql_column])
67
+ end
68
+
69
+ raise 'search method must return an ActiveRecord::Relation object' unless collection.kind_of?(ActiveRecord::Relation)
70
+ end
71
+
72
+ collection
73
+ end
74
+
75
+ def search_column(collection, value, column, sql_column)
76
+ Rails.logger.info "COLUMN TOOL: search_column #{column.to_s} #{value} #{sql_column}"
77
+
78
+ Effective::Resource.new(collection)
79
+ .search(column[:name], value, as: column[:as], fuzzy: column[:search][:fuzzy], sql_column: sql_column)
80
+ end
81
+
82
+ def paginate(collection)
83
+ collection.page(datatable.page).per(datatable.per_page)
84
+ end
85
+
86
+ # Not every ActiveRecord query will work when calling the simple .count
87
+ # Custom selects:
88
+ # User.select(:email, :first_name).count will throw an error
89
+ # Grouped Queries:
90
+ # User.all.group(:email).count will return a Hash
91
+ def size(collection)
92
+ count = (collection.size rescue nil)
93
+
94
+ case count
95
+ when Integer
96
+ count
97
+ when Hash
98
+ count.size # This represents the number of displayed datatable rows, not the sum all groups (which might be more)
99
+ else
100
+ if collection.klass.connection.respond_to?(:unprepared_statement)
101
+ collection_sql = collection.klass.connection.unprepared_statement { collection.to_sql }
102
+ (collection.klass.connection.exec_query("SELECT COUNT(*) FROM (#{collection_sql}) AS datatables_total_count").rows[0][0] rescue 1)
103
+ else
104
+ (collection.klass.connection.exec_query("SELECT COUNT(*) FROM (#{collection.to_sql}) AS datatables_total_count").rows[0][0] rescue 1)
105
+ end.to_i
106
+ end
107
+ end
108
+
109
+ end
110
+ end
@@ -0,0 +1,28 @@
1
+ module Effective
2
+
3
+ class DatatableDslTool
4
+ attr_reader :datatable
5
+ attr_reader :view
6
+
7
+ include Effective::EffectiveDatatable::Dsl::BulkActions
8
+ include Effective::EffectiveDatatable::Dsl::Charts
9
+ include Effective::EffectiveDatatable::Dsl::Datatable
10
+ include Effective::EffectiveDatatable::Dsl::Filters
11
+
12
+ def initialize(datatable)
13
+ @datatable = datatable
14
+ @view = datatable.view
15
+ end
16
+
17
+ def method_missing(method, *args)
18
+ if datatable.respond_to?(method)
19
+ datatable.send(method, *args)
20
+ elsif view.respond_to?(method)
21
+ view.send(method, *args)
22
+ else
23
+ super
24
+ end
25
+ end
26
+
27
+ end
28
+ end
@@ -0,0 +1,142 @@
1
+ module Effective
2
+ # The collection is an Array of Arrays
3
+ class DatatableValueTool
4
+ attr_reader :datatable
5
+ attr_reader :columns
6
+
7
+ def initialize(datatable)
8
+ @datatable = datatable
9
+
10
+ if datatable.array_collection?
11
+ @columns = datatable.columns
12
+ else
13
+ @columns = datatable.columns.select { |_, col| col[:sql_column].blank? }
14
+ end
15
+ end
16
+
17
+ def searched
18
+ @searched ||= datatable.search.select { |name, _| columns.key?(name) }
19
+ end
20
+
21
+ def ordered
22
+ @ordered ||= columns[datatable.order_name]
23
+ end
24
+
25
+ def order(collection)
26
+ return collection unless ordered.present?
27
+
28
+ collection = if ordered[:sort_method]
29
+ datatable.dsl_tool.instance_exec(collection, datatable.order_direction, ordered, ordered[:index], &ordered[:sort_method])
30
+ else
31
+ order_column(collection, datatable.order_direction, ordered, ordered[:index])
32
+ end
33
+
34
+ raise 'sort method must return an Array' unless collection.kind_of?(Array)
35
+
36
+ collection
37
+ end
38
+
39
+ def order_column(collection, direction, column, index)
40
+ Rails.logger.info "VALUE TOOL: order_column :#{column.to_s} :#{direction} #{index}"
41
+
42
+ if direction == :asc
43
+ collection.sort! do |x, y|
44
+ x[index] <=> y[index] || x[index].to_s <=> y[index].to_s || 0
45
+ end
46
+ else
47
+ collection.sort! do |x, y|
48
+ y[index] <=> x[index] || y[index].to_s <=> x[index].to_s || 0
49
+ end
50
+ end
51
+
52
+ collection
53
+ end
54
+
55
+ def search(collection)
56
+ searched.each do |name, value|
57
+ column = columns[name]
58
+
59
+ collection = if column[:search_method]
60
+ datatable.dsl_tool.instance_exec(collection, value, column, column[:index], &column[:search_method])
61
+ else
62
+ search_column(collection, value, column, column[:index])
63
+ end
64
+
65
+ raise 'search method must return an Array object' unless collection.kind_of?(Array)
66
+ end
67
+
68
+ collection
69
+ end
70
+
71
+ def search_column(collection, value, column, index)
72
+ Rails.logger.info "VALUE TOOL: search_column #{column.to_s} #{value} #{index}"
73
+
74
+ fuzzy = column[:search][:fuzzy]
75
+ term = Effective::Attribute.new(column[:as]).parse(value, name: column[:name])
76
+ term_downcased = term.downcase if fuzzy && term.kind_of?(String)
77
+
78
+ if term == 'nil'
79
+ return (collection.select! { |row| row[index].nil? } || collection)
80
+ end
81
+
82
+ # See effective_resources gem search() method # relation.rb
83
+ collection.select! do |row|
84
+ case column[:as]
85
+ when :duration
86
+ if fuzzy && (term % 60 == 0) && value.to_s.include?('m') == false
87
+ if term < 0
88
+ row[index] <= term && row[index] > (term - 60)
89
+ else
90
+ row[index] >= term && row[index] < (term + 60)
91
+ end
92
+ else
93
+ row[index] == term
94
+ end
95
+ when :decimal, :currency
96
+ if fuzzy && (term.round(0) == term) && value.to_s.include?('.') == false
97
+ if term < 0
98
+ row[index] <= term && row[index] > (term - 1.0)
99
+ else
100
+ row[index] >= term && row[index] < (term + 1.0)
101
+ end
102
+ else
103
+ row[index] == term
104
+ end
105
+ when :resource
106
+ Array(row[index]).any? do |resource|
107
+ if term.kind_of?(Integer) && resource.respond_to?(:id)
108
+ resource.id == term || resource.to_param == term
109
+ elsif term.kind_of?(Array) && resource.respond_to?(:id)
110
+ term.any? { |term| resource.id == term || resource.to_param == term || resource.to_param == value }
111
+ else
112
+ fuzzy ? resource.to_s.downcase == term_downcased : resource.to_s == term
113
+ end
114
+ end
115
+ when :string, :text, :email
116
+ if fuzzy
117
+ row[index].to_s.downcase.include?(term_downcased)
118
+ else
119
+ row[index] == term
120
+ end
121
+ else
122
+ row[index] == term
123
+ end
124
+ end || collection
125
+ end
126
+
127
+ def paginate(collection)
128
+ Kaminari.paginate_array(collection).page(datatable.page).per(datatable.per_page)
129
+ end
130
+
131
+ def size(collection)
132
+ collection.size
133
+ end
134
+
135
+ end
136
+ end
137
+
138
+ # [
139
+ # [1, 'title 1'],
140
+ # [2, 'title 2'],
141
+ # [3, 'title 3']
142
+ # ]
@@ -0,0 +1,25 @@
1
+ module Effective
2
+ module EffectiveDatatable
3
+ module Attributes
4
+
5
+ private
6
+
7
+ def initial_attributes(args)
8
+ raise "#{self.class.name}.new() expected Hash like arguments" unless args.kind_of?(Hash)
9
+ args
10
+ end
11
+
12
+ def load_attributes!
13
+ if datatables_ajax_request?
14
+ raise 'Expected attributes cookie to be present' unless cookie
15
+ @attributes = cookie[:attributes]
16
+ end
17
+
18
+ unless datatables_ajax_request?
19
+ @attributes[:controller_namespace] ||= view.controller_path.split('/')[0...-1].join('/').presence
20
+ end
21
+ end
22
+
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,38 @@
1
+ module Effective
2
+ module EffectiveDatatable
3
+ module Collection
4
+
5
+ # Used for authorization. We authorize with authorized?(:index, collection_class)
6
+ def collection_class
7
+ @collection_class # Will be either User/Post/etc or Array
8
+ end
9
+
10
+ def active_record_collection?
11
+ @active_record_collection == true
12
+ end
13
+
14
+ def array_collection?
15
+ @array_collection == true
16
+ end
17
+
18
+ private
19
+
20
+ def load_collection!
21
+ raise 'No collection defined. Please add a collection with collection do ... end' if collection.nil?
22
+
23
+ @collection_class = (collection.respond_to?(:klass) ? collection.klass : self.class)
24
+ @active_record_collection = (collection.ancestors.include?(ActiveRecord::Base) rescue false)
25
+ @array_collection = (collection.kind_of?(Array) && (collection.length == 0 || collection.first.kind_of?(Array)))
26
+
27
+ unless active_record_collection? || array_collection?
28
+ raise "Unsupported collection type. Expecting an ActiveRecord class, ActiveRecord relation, or an Array of Arrays [[1, 'foo'], [2, 'bar']]"
29
+ end
30
+
31
+ _scopes.each do |scope, _|
32
+ raise "invalid scope: :#{scope}. The collection must respond to :#{scope}" unless collection.respond_to?(scope)
33
+ end
34
+ end
35
+
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,154 @@
1
+ module Effective
2
+ module EffectiveDatatable
3
+ module Compute
4
+ BLANK = ''.freeze
5
+
6
+ private
7
+
8
+ # So the idea here is that we want to do as much as possible on the database in ActiveRecord
9
+ # And then run any array_columns through in post-processed results
10
+ def compute
11
+ col = collection
12
+
13
+ # Assign total records
14
+ @total_records = (active_record_collection? ? column_tool.size(col) : value_tool.size(col))
15
+
16
+ # Apply scope
17
+ col = column_tool.scope(col)
18
+
19
+ # Apply column searching
20
+ col = column_tool.search(col)
21
+ @display_records = column_tool.size(col) unless value_tool.searched.present?
22
+
23
+ # Apply column ordering
24
+ col = column_tool.order(col)
25
+
26
+ # Arrayize if we have value tool work to do
27
+ col = arrayize(col) if value_tool.searched.present? || value_tool.ordered.present?
28
+
29
+ # Apply value searching
30
+ col = value_tool.search(col)
31
+ @display_records = value_tool.size(col) if value_tool.searched.present?
32
+
33
+ # Apply value ordering
34
+ col = value_tool.order(col)
35
+
36
+ # Apply pagination
37
+ col = col.kind_of?(Array) ? value_tool.paginate(col) : column_tool.paginate(col)
38
+
39
+ # Arrayize the searched, ordered, paginated results
40
+ col = arrayize(col) unless col.kind_of?(Array)
41
+
42
+ # Assign display records
43
+ @display_records ||= @total_records
44
+
45
+ # Compute aggregate data
46
+ @aggregates_data = aggregate(col) if _aggregates.present?
47
+
48
+ # Charts too
49
+ @charts_data = chart(col) if _charts.present?
50
+
51
+ # Format all results
52
+ format(col)
53
+
54
+ # Finalize hook
55
+ finalize(col)
56
+ end
57
+
58
+ def arrayize(collection)
59
+ collection.map do |obj|
60
+ columns.map do |name, opts|
61
+ if state[:visible][name] == false && (name != order_name) # Sort by invisible array column
62
+ BLANK
63
+ elsif opts[:partial] || (opts[:format] && !opts[:compute])
64
+ active_record_collection? ? obj : obj[opts[:index]]
65
+ elsif opts[:compute]
66
+ dsl_tool.instance_exec(obj, (active_record_collection? ? collection : obj[opts[:index]]), &opts[:compute])
67
+ elsif opts[:as] == :effective_obfuscation
68
+ obj.to_param
69
+ elsif array_collection?
70
+ obj[opts[:index]]
71
+ elsif opts[:sql_as_column]
72
+ obj[name] || obj.send(name)
73
+ else
74
+ obj.send(name)
75
+ end
76
+ end
77
+ end
78
+ end
79
+
80
+ def aggregate(collection)
81
+ cols = collection.transpose
82
+
83
+ _aggregates.map do |_, aggregate|
84
+ columns.map do |name, opts|
85
+ next if state[:visible][name] == false && datatables_ajax_request?
86
+
87
+ values = cols[opts[:index]] || []
88
+
89
+ if state[:visible][name] == false
90
+ BLANK
91
+ elsif aggregate[:compute]
92
+ dsl_tool.instance_exec(values, columns[name], &aggregate[:compute])
93
+ else
94
+ format_column(aggregate_column(values, opts, aggregate), opts)
95
+ end
96
+ end.compact
97
+ end
98
+ end
99
+
100
+ def aggregate_column(values, column, aggregate)
101
+ labeled = false
102
+ length = values.length
103
+ values = values.reject { |value| value.nil? }
104
+
105
+ if [:bulk_actions, :actions].include?(column[:as]) || length == 0
106
+ return BLANK
107
+ end
108
+
109
+ case aggregate[:name]
110
+ when :total
111
+ if values.all? { |value| value.kind_of?(Numeric) }
112
+ values.sum
113
+ elsif values.all? { |value| value == true || value == false }
114
+ "#{values.count { |val| val == true }} / #{values.count { |val| val == false}}"
115
+ elsif !labeled
116
+ labeled = aggregate[:label]
117
+ elsif values.any? { |value| value.kind_of?(String) == false }
118
+ "#{values.flatten.count} total"
119
+ end
120
+ when :average
121
+ if values.all? { |value| value.kind_of?(Numeric) }
122
+ values.sum / [length, 1].max
123
+ elsif values.all? { |value| value == true || value == false }
124
+ values.count { |val| val == true } >= (length / 2) ? true : false
125
+ elsif !labeled
126
+ labeled = aggregate[:label]
127
+ elsif values.any? { |value| value.kind_of?(String) == false }
128
+ '-'
129
+ end
130
+ else
131
+ raise 'not implemented'
132
+ end || BLANK
133
+ end
134
+
135
+ def chart(collection)
136
+ _charts.inject({}) do |retval, (name, chart)|
137
+ retval[name] = {
138
+ as: chart[:as],
139
+ data: dsl_tool.instance_exec(collection, &chart[:compute]),
140
+ name: chart[:name],
141
+ options: chart[:options]
142
+ }
143
+
144
+ unless retval[name][:data].kind_of?(Array) && retval[name][:data].first.kind_of?(Array)
145
+ raise "invalid chart :#{name}. The block must return an Array of Arrays"
146
+ end
147
+
148
+ retval
149
+ end
150
+ end
151
+
152
+ end
153
+ end
154
+ end