ajax-datatables-rails 0.4.2 → 1.3.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 (69) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +120 -0
  3. data/.rubocop.yml +17 -7
  4. data/Appraisals +15 -20
  5. data/CHANGELOG.md +56 -4
  6. data/Gemfile +0 -5
  7. data/Guardfile +16 -0
  8. data/README.md +223 -97
  9. data/Rakefile +1 -0
  10. data/ajax-datatables-rails.gemspec +24 -20
  11. data/bin/_guard-core +29 -0
  12. data/bin/appraisal +29 -0
  13. data/bin/guard +29 -0
  14. data/bin/rake +29 -0
  15. data/bin/rspec +29 -0
  16. data/bin/rubocop +29 -0
  17. data/doc/migrate.md +97 -0
  18. data/doc/webpack.md +7 -2
  19. data/gemfiles/{rails_5.2.0.gemfile → rails_5.2.4.gemfile} +3 -5
  20. data/gemfiles/{rails_5.0.7.gemfile → rails_6.0.3.gemfile} +4 -6
  21. data/gemfiles/{rails_5.1.6.gemfile → rails_6.1.0.gemfile} +4 -6
  22. data/lib/ajax-datatables-rails.rb +12 -1
  23. data/lib/ajax-datatables-rails/active_record.rb +7 -0
  24. data/lib/ajax-datatables-rails/base.rb +47 -46
  25. data/lib/ajax-datatables-rails/datatable.rb +6 -0
  26. data/lib/ajax-datatables-rails/datatable/column.rb +65 -20
  27. data/lib/ajax-datatables-rails/datatable/column/date_filter.rb +12 -21
  28. data/lib/ajax-datatables-rails/datatable/column/order.rb +1 -1
  29. data/lib/ajax-datatables-rails/datatable/column/search.rb +36 -23
  30. data/lib/ajax-datatables-rails/datatable/datatable.rb +16 -6
  31. data/lib/ajax-datatables-rails/datatable/simple_order.rb +23 -10
  32. data/lib/ajax-datatables-rails/datatable/simple_search.rb +2 -0
  33. data/lib/ajax-datatables-rails/error.rb +9 -0
  34. data/lib/ajax-datatables-rails/orm.rb +6 -0
  35. data/lib/ajax-datatables-rails/orm/active_record.rb +11 -12
  36. data/lib/ajax-datatables-rails/version.rb +13 -1
  37. data/lib/generators/rails/templates/datatable.rb +1 -1
  38. data/spec/ajax-datatables-rails/base_spec.rb +95 -79
  39. data/spec/ajax-datatables-rails/datatable/column_spec.rb +83 -12
  40. data/spec/ajax-datatables-rails/datatable/datatable_spec.rb +62 -24
  41. data/spec/ajax-datatables-rails/datatable/simple_order_spec.rb +32 -12
  42. data/spec/ajax-datatables-rails/orm/active_record_filter_records_spec.rb +329 -221
  43. data/spec/ajax-datatables-rails/orm/active_record_paginate_records_spec.rb +5 -6
  44. data/spec/ajax-datatables-rails/orm/active_record_sort_records_spec.rb +12 -11
  45. data/spec/ajax-datatables-rails/orm/active_record_spec.rb +3 -4
  46. data/spec/install_oracle.sh +9 -3
  47. data/spec/spec_helper.rb +10 -21
  48. data/spec/support/datatables/complex_datatable.rb +29 -0
  49. data/spec/support/datatables/complex_datatable_array.rb +14 -0
  50. data/spec/support/{datatable_cond_date.rb → datatables/datatable_cond_date.rb} +0 -0
  51. data/spec/support/{datatable_cond_numeric.rb → datatables/datatable_cond_numeric.rb} +1 -1
  52. data/spec/support/{datatable_cond_proc.rb → datatables/datatable_cond_proc.rb} +0 -0
  53. data/spec/support/datatables/datatable_cond_string.rb +41 -0
  54. data/spec/support/datatables/datatable_cond_unknown.rb +5 -0
  55. data/spec/support/{datatable_order_nulls_last.rb → datatables/datatable_order_nulls_last.rb} +0 -0
  56. data/spec/support/{test_helpers.rb → helpers/params.rb} +14 -41
  57. data/spec/support/{test_models.rb → models/user.rb} +0 -0
  58. metadata +75 -74
  59. data/.travis.yml +0 -80
  60. data/gemfiles/rails_4.0.13.gemfile +0 -14
  61. data/gemfiles/rails_4.1.16.gemfile +0 -14
  62. data/gemfiles/rails_4.2.10.gemfile +0 -14
  63. data/lib/ajax-datatables-rails/config.rb +0 -31
  64. data/lib/ajax_datatables_rails.rb +0 -15
  65. data/lib/generators/datatable/config_generator.rb +0 -19
  66. data/lib/generators/datatable/templates/ajax_datatables_rails_config.rb +0 -12
  67. data/spec/ajax-datatables-rails/configuration_spec.rb +0 -43
  68. data/spec/ajax-datatables-rails/extended_spec.rb +0 -20
  69. data/spec/support/datatable_cond_string.rb +0 -23
@@ -2,23 +2,21 @@
2
2
 
3
3
  module AjaxDatatablesRails
4
4
  class Base
5
- extend Forwardable
6
5
 
7
- attr_reader :view, :options
8
- def_delegator :@view, :params
6
+ class_attribute :db_adapter, default: ActiveRecord::Base.connection.adapter_name.downcase.to_sym
7
+ class_attribute :nulls_last, default: false
9
8
 
10
- GLOBAL_SEARCH_DELIMITER = ' '
9
+ attr_reader :params, :options, :datatable
11
10
 
12
- def initialize(view, options = {})
13
- @view = view
14
- @options = options
15
- load_orm_extension
16
- end
11
+ GLOBAL_SEARCH_DELIMITER = ' '
17
12
 
18
- def datatable
19
- @datatable ||= Datatable::Datatable.new(self)
13
+ def initialize(params, options = {})
14
+ @params = params
15
+ @options = options
16
+ @datatable = Datatable::Datatable.new(self)
20
17
  end
21
18
 
19
+ # User defined methods
22
20
  def view_columns
23
21
  raise(NotImplementedError, view_columns_error_text)
24
22
  end
@@ -31,22 +29,49 @@ module AjaxDatatablesRails
31
29
  raise(NotImplementedError, data_error_text)
32
30
  end
33
31
 
32
+ # ORM defined methods
33
+ def fetch_records
34
+ get_raw_records
35
+ end
36
+
37
+ def filter_records(records)
38
+ raise(NotImplementedError)
39
+ end
40
+
41
+ def sort_records(records)
42
+ raise(NotImplementedError)
43
+ end
44
+
45
+ def paginate_records(records)
46
+ raise(NotImplementedError)
47
+ end
48
+
49
+ # User overides
34
50
  def additional_data
35
51
  {}
36
52
  end
37
53
 
54
+ # JSON structure sent to jQuery DataTables
38
55
  def as_json(*)
39
56
  {
40
- recordsTotal: records_total_count,
57
+ recordsTotal: records_total_count,
41
58
  recordsFiltered: records_filtered_count,
42
- data: sanitize(data)
43
- }.merge(get_additional_data)
59
+ data: sanitize_data(data),
60
+ }.merge(additional_data)
44
61
  end
45
62
 
46
- def records
47
- @records ||= retrieve_records
63
+ # User helper methods
64
+ def column_id(name)
65
+ view_columns.keys.index(name.to_sym)
66
+ end
67
+
68
+ def column_data(column)
69
+ id = column_id(column)
70
+ params.dig('columns', id.to_s, 'search', 'value')
48
71
  end
49
72
 
73
+ private
74
+
50
75
  # helper methods
51
76
  def connected_columns
52
77
  @connected_columns ||= begin
@@ -68,26 +93,7 @@ module AjaxDatatablesRails
68
93
  end
69
94
  end
70
95
 
71
- private
72
-
73
- # This method is necessary for smooth transition from
74
- # `additinonal_datas` method to `additional_data`
75
- # without breaking change.
76
- def get_additional_data
77
- if respond_to?(:additional_datas)
78
- puts <<-WARNING
79
- `additional_datas` has been deprecated and
80
- will be removed in next major version update!
81
- Please use `additional_data` instead.
82
- WARNING
83
-
84
- additional_datas
85
- else
86
- additional_data
87
- end
88
- end
89
-
90
- def sanitize(data)
96
+ def sanitize_data(data)
91
97
  data.map do |record|
92
98
  if record.is_a?(Array)
93
99
  record.map { |td| ERB::Util.html_escape(td) }
@@ -97,6 +103,11 @@ module AjaxDatatablesRails
97
103
  end
98
104
  end
99
105
 
106
+ # called from within #data
107
+ def records
108
+ @records ||= retrieve_records
109
+ end
110
+
100
111
  def retrieve_records
101
112
  records = fetch_records
102
113
  records = filter_records(records)
@@ -113,16 +124,6 @@ module AjaxDatatablesRails
113
124
  filter_records(fetch_records).count(:all)
114
125
  end
115
126
 
116
- # Private helper methods
117
- def load_orm_extension
118
- case AjaxDatatablesRails.config.orm
119
- when :active_record
120
- extend ORM::ActiveRecord
121
- when :mongoid
122
- nil
123
- end
124
- end
125
-
126
127
  def global_search_delimiter
127
128
  GLOBAL_SEARCH_DELIMITER
128
129
  end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AjaxDatatablesRails
4
+ module Datatable
5
+ end
6
+ end
@@ -4,28 +4,23 @@ module AjaxDatatablesRails
4
4
  module Datatable
5
5
  class Column
6
6
 
7
- DB_ADAPTER_TYPE_CAST = {
8
- mysql: 'CHAR',
9
- mysql2: 'CHAR',
10
- sqlite: 'TEXT',
11
- sqlite3: 'TEXT',
12
- oracle: 'VARCHAR2(4000)',
13
- oracleenhanced: 'VARCHAR2(4000)'
14
- }.freeze
15
-
16
- attr_reader :datatable, :index, :options
17
- attr_writer :search
18
-
19
7
  include Search
20
8
  include Order
21
- prepend DateFilter unless AjaxDatatablesRails.old_rails?
9
+ include DateFilter
22
10
 
11
+ attr_reader :datatable, :index, :options
12
+ attr_writer :search
23
13
 
24
14
  def initialize(datatable, index, options)
25
15
  @datatable = datatable
26
16
  @index = index
27
17
  @options = options
28
- @view_column = datatable.view_columns[options[:data].to_sym]
18
+ @view_column = datatable.view_columns[column_name]
19
+ validate_settings!
20
+ end
21
+
22
+ def column_name
23
+ @column_name ||= options[:data]&.to_sym
29
24
  end
30
25
 
31
26
  def data
@@ -52,26 +47,76 @@ module AjaxDatatablesRails
52
47
  !source.include?('.')
53
48
  end
54
49
 
55
- # Add formater option to allow modification of the value
50
+ # Add formatter option to allow modification of the value
56
51
  # before passing it to the database
57
- def formater
58
- @view_column[:formater]
52
+ def formatter
53
+ @view_column[:formatter]
59
54
  end
60
55
 
61
- def formated_value
62
- formater ? formater.call(search.value) : search.value
56
+ def formatted_value
57
+ formatter ? formatter.call(search.value) : search.value
63
58
  end
64
59
 
65
60
  private
66
61
 
62
+ TYPE_CAST_DEFAULT = 'VARCHAR'
63
+ TYPE_CAST_MYSQL = 'CHAR'
64
+ TYPE_CAST_SQLITE = 'TEXT'
65
+ TYPE_CAST_ORACLE = 'VARCHAR2(4000)'
66
+ TYPE_CAST_SQLSERVER = 'VARCHAR(4000)'
67
+
68
+ DB_ADAPTER_TYPE_CAST = {
69
+ mysql: TYPE_CAST_MYSQL,
70
+ mysql2: TYPE_CAST_MYSQL,
71
+ sqlite: TYPE_CAST_SQLITE,
72
+ sqlite3: TYPE_CAST_SQLITE,
73
+ oracle: TYPE_CAST_ORACLE,
74
+ oracleenhanced: TYPE_CAST_ORACLE,
75
+ sqlserver: TYPE_CAST_SQLSERVER,
76
+ }.freeze
77
+
78
+ private_constant :TYPE_CAST_DEFAULT
79
+ private_constant :TYPE_CAST_MYSQL
80
+ private_constant :TYPE_CAST_SQLITE
81
+ private_constant :TYPE_CAST_ORACLE
82
+ private_constant :TYPE_CAST_SQLSERVER
83
+ private_constant :DB_ADAPTER_TYPE_CAST
84
+
67
85
  def type_cast
68
- @type_cast ||= (DB_ADAPTER_TYPE_CAST[AjaxDatatablesRails.config.db_adapter] || 'VARCHAR')
86
+ @type_cast ||= DB_ADAPTER_TYPE_CAST.fetch(datatable.db_adapter, TYPE_CAST_DEFAULT)
69
87
  end
70
88
 
71
89
  def casted_column
72
90
  @casted_column ||= ::Arel::Nodes::NamedFunction.new('CAST', [table[field].as(type_cast)])
73
91
  end
74
92
 
93
+ def validate_settings!
94
+ raise AjaxDatatablesRails::Error::InvalidSearchColumn, "Unknown column. Check that `data` field is filled on JS side with the column name" if column_name.empty?
95
+ raise AjaxDatatablesRails::Error::InvalidSearchColumn, "Check that column '#{column_name}' exists in view_columns" unless valid_search_column?(column_name)
96
+ raise AjaxDatatablesRails::Error::InvalidSearchCondition, cond unless valid_search_condition?(cond)
97
+ end
98
+
99
+ def valid_search_column?(column_name)
100
+ !datatable.view_columns[column_name].nil?
101
+ end
102
+
103
+ VALID_SEARCH_CONDITIONS = [
104
+ # String condition
105
+ :start_with, :end_with, :like, :string_eq, :string_in, :null_value,
106
+ # Numeric condition
107
+ :eq, :not_eq, :lt, :gt, :lteq, :gteq, :in,
108
+ # Date condition
109
+ :date_range
110
+ ].freeze
111
+
112
+ private_constant :VALID_SEARCH_CONDITIONS
113
+
114
+ def valid_search_condition?(cond)
115
+ return true if cond.is_a?(Proc)
116
+
117
+ VALID_SEARCH_CONDITIONS.include?(cond)
118
+ end
119
+
75
120
  end
76
121
  end
77
122
  end
@@ -5,6 +5,8 @@ module AjaxDatatablesRails
5
5
  class Column
6
6
  module DateFilter
7
7
 
8
+ RANGE_DELIMITER = '-'
9
+
8
10
  class DateRange
9
11
  attr_reader :begin, :end
10
12
 
@@ -20,55 +22,44 @@ module AjaxDatatablesRails
20
22
 
21
23
  # Add delimiter option to handle range search
22
24
  def delimiter
23
- @view_column[:delimiter] || '-'
24
- end
25
-
26
- def empty_range_search?
27
- (formated_value == delimiter) || (range_start.blank? && range_end.blank?)
25
+ @delimiter ||= @view_column.fetch(:delimiter, RANGE_DELIMITER)
28
26
  end
29
27
 
30
28
  # A range value is in form '<range_start><delimiter><range_end>'
31
29
  # This returns <range_start>
32
30
  def range_start
33
- @range_start ||= formated_value.split(delimiter)[0]
31
+ @range_start ||= formatted_value.split(delimiter)[0]
34
32
  end
35
33
 
36
34
  # A range value is in form '<range_start><delimiter><range_end>'
37
35
  # This returns <range_end>
38
36
  def range_end
39
- @range_end ||= formated_value.split(delimiter)[1]
37
+ @range_end ||= formatted_value.split(delimiter)[1]
38
+ end
39
+
40
+ def empty_range_search?
41
+ (formatted_value == delimiter) || (range_start.blank? && range_end.blank?)
40
42
  end
41
43
 
42
44
  # Do a range search
43
45
  def date_range_search
44
46
  return nil if empty_range_search?
47
+
45
48
  table[field].between(DateRange.new(range_start_casted, range_end_casted))
46
49
  end
47
50
 
48
51
  private
49
52
 
50
- def non_regex_search
51
- if cond == :date_range
52
- date_range_search
53
- else
54
- super
55
- end
56
- end
57
-
58
53
  def range_start_casted
59
54
  range_start.blank? ? parse_date('01/01/1970') : parse_date(range_start)
60
55
  end
61
56
 
62
57
  def range_end_casted
63
- range_end.blank? ? Time.current : parse_date("#{range_end} 23:59:59")
58
+ range_end.blank? ? parse_date('9999-12-31 23:59:59') : parse_date("#{range_end} 23:59:59")
64
59
  end
65
60
 
66
61
  def parse_date(date)
67
- if Time.zone
68
- Time.zone.parse(date)
69
- else
70
- Time.parse(date)
71
- end
62
+ Time.zone ? Time.zone.parse(date) : Time.parse(date)
72
63
  end
73
64
 
74
65
  end
@@ -11,7 +11,7 @@ module AjaxDatatablesRails
11
11
 
12
12
  # Add sort_field option to allow overriding of sort field
13
13
  def sort_field
14
- @view_column[:sort_field] || field
14
+ @view_column.fetch(:sort_field, field)
15
15
  end
16
16
 
17
17
  def sort_query
@@ -5,19 +5,21 @@ module AjaxDatatablesRails
5
5
  class Column
6
6
  module Search
7
7
 
8
- SMALLEST_PQ_INTEGER = -2147483648
9
- LARGEST_PQ_INTEGER = 2147483647
8
+ SMALLEST_PQ_INTEGER = -2_147_483_648
9
+ LARGEST_PQ_INTEGER = 2_147_483_647
10
+ NOT_NULL_VALUE = '!NULL'
11
+ EMPTY_VALUE = ''
10
12
 
11
13
  def searchable?
12
14
  @view_column.fetch(:searchable, true)
13
15
  end
14
16
 
15
17
  def cond
16
- @view_column[:cond] || :like
18
+ @view_column.fetch(:cond, :like)
17
19
  end
18
20
 
19
21
  def filter
20
- @view_column[:cond].call(self, formated_value)
22
+ @view_column[:cond].call(self, formatted_value)
21
23
  end
22
24
 
23
25
  def search
@@ -46,64 +48,75 @@ module AjaxDatatablesRails
46
48
  # The solution is to bypass regex_search and use non_regex_search with :in operator
47
49
  def regex_search
48
50
  if use_regex?
49
- ::Arel::Nodes::Regexp.new((custom_field? ? field : table[field]), ::Arel::Nodes.build_quoted(formated_value))
51
+ ::Arel::Nodes::Regexp.new((custom_field? ? field : table[field]), ::Arel::Nodes.build_quoted(formatted_value))
50
52
  else
51
53
  non_regex_search
52
54
  end
53
55
  end
54
56
 
57
+ # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity
55
58
  def non_regex_search
56
59
  case cond
57
60
  when Proc
58
61
  filter
59
62
  when :eq, :not_eq, :lt, :gt, :lteq, :gteq, :in
60
- is_searchable_integer? ? numeric_search : empty_search
61
- when :null_value
62
- null_value_search
63
+ searchable_integer? ? raw_search(cond) : empty_search
63
64
  when :start_with
64
- casted_column.matches("#{formated_value}%")
65
+ casted_column.matches("#{formatted_value}%")
65
66
  when :end_with
66
- casted_column.matches("%#{formated_value}")
67
+ casted_column.matches("%#{formatted_value}")
67
68
  when :like
68
- casted_column.matches("%#{formated_value}%")
69
+ casted_column.matches("%#{formatted_value}%")
70
+ when :string_eq
71
+ raw_search(:eq)
72
+ when :string_in
73
+ raw_search(:in)
74
+ when :null_value
75
+ null_value_search
76
+ when :date_range
77
+ date_range_search
69
78
  end
70
79
  end
80
+ # rubocop:enable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity
71
81
 
72
82
  def null_value_search
73
- if formated_value == '!NULL'
83
+ if formatted_value == NOT_NULL_VALUE
74
84
  table[field].not_eq(nil)
75
85
  else
76
86
  table[field].eq(nil)
77
87
  end
78
88
  end
79
89
 
80
- def numeric_search
90
+ def raw_search(cond)
81
91
  if custom_field?
82
- ::Arel::Nodes::SqlLiteral.new(field).eq(formated_value)
92
+ ::Arel::Nodes::SqlLiteral.new(field).eq(formatted_value)
83
93
  else
84
- table[field].send(cond, formated_value)
94
+ table[field].send(cond, formatted_value)
85
95
  end
86
96
  end
87
97
 
88
98
  def empty_search
89
- casted_column.matches('')
99
+ casted_column.matches(EMPTY_VALUE)
90
100
  end
91
101
 
92
- def is_searchable_integer?
93
- if formated_value.is_a?(Array)
94
- valids = formated_value.map { |v| is_integer?(v) && !is_out_of_range?(v) }
102
+ def searchable_integer?
103
+ if formatted_value.is_a?(Array)
104
+ valids = formatted_value.map { |v| integer?(v) && !out_of_range?(v) }
95
105
  !valids.include?(false)
96
106
  else
97
- is_integer?(formated_value) && !is_out_of_range?(formated_value)
107
+ integer?(formatted_value) && !out_of_range?(formatted_value)
98
108
  end
99
109
  end
100
110
 
101
- def is_out_of_range?(search_value)
111
+ def out_of_range?(search_value)
102
112
  Integer(search_value) > LARGEST_PQ_INTEGER || Integer(search_value) < SMALLEST_PQ_INTEGER
103
113
  end
104
114
 
105
- def is_integer?(string)
106
- true if Integer(string) rescue false
115
+ def integer?(string)
116
+ Integer(string)
117
+ true
118
+ rescue ArgumentError
119
+ false
107
120
  end
108
121
 
109
122
  end