effective_datatables 2.12.2 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
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