diff --git a/.codeclimate.yml b/.codeclimate.yml
index 42a049b1..6cdba48a 100644
--- a/.codeclimate.yml
+++ b/.codeclimate.yml
@@ -1,26 +1,8 @@
---
-engines:
- duplication:
- enabled: true
- config:
- languages:
- - ruby
- - javascript
- - python
- - php
- fixme:
- enabled: true
+plugins:
rubocop:
enabled: true
-ratings:
- paths:
- - "**.inc"
- - "**.js"
- - "**.jsx"
- - "**.module"
- - "**.php"
- - "**.py"
- - "**.rb"
-exclude_paths:
-- spec/
-- lib/generators/rails/templates/
+ channel: rubocop-1-31-0
+exclude_patterns:
+ - spec/
+ - lib/generators/rails/templates/
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 00000000..9d1a58d9
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,128 @@
+---
+name: CI
+
+on:
+ push:
+ branches:
+ - '**'
+ pull_request:
+ branches:
+ - '**'
+ schedule:
+ - cron: '0 4 1 * *'
+ # Run workflow manually
+ workflow_dispatch:
+
+jobs:
+ rubocop:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: '3.1'
+
+ - name: Bundler
+ run: bundle install
+
+ - name: Rubocop
+ run: bin/rubocop
+
+ rspec:
+ runs-on: ubuntu-latest
+
+ env: # $BUNDLE_GEMFILE must be set at the job level, so it is set for all steps
+ BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/${{ matrix.rails }}_with_${{ matrix.adapter }}.gemfile
+
+ services:
+ postgres:
+ image: 'postgres:16'
+ ports: ['5432:5432']
+ env:
+ POSTGRES_PASSWORD: postgres
+ POSTGRES_DB: ajax_datatables_rails
+ options: >-
+ --health-cmd pg_isready
+ --health-interval 10s
+ --health-timeout 5s
+ --health-retries 5
+
+ # Using docker image fails with
+ # invalid reference format
+ # mariadb:
+ # image: 'mariadb:10.3'
+ # ports: ['3306:3306']
+ # env:
+ # MYSQL_ROOT_PASSWORD: root
+ # MYSQL_DATABASE: ajax_datatables_rails
+ # options: >-
+ # --health-cmd 'mysqladmin ping'
+ # --health-interval 10s
+ # --health-timeout 5s
+ # --health-retries 3
+
+ strategy:
+ fail-fast: false
+ matrix:
+ ruby:
+ - '3.4'
+ - '3.3'
+ - '3.2'
+ - '3.1'
+ - 'head'
+ rails:
+ - rails_8.0
+ - rails_7.2
+ - rails_7.1
+ adapter:
+ - sqlite3
+ - postgresql
+ - mysql2
+ - postgis
+ # Disabled for now:
+ # Rails 7.0: trilogy_auth_recv: caching_sha2_password requires either TCP with TLS or a unix socket: TRILOGY_UNSUPPORTED
+ # Rails 7.1: unknown keyword: :uses_transaction
+ # Rails 7.2: NotImplementedError
+ # - trilogy
+ exclude:
+ # Rails 8.0 needs Ruby > 3.2
+ - rails: 'rails_8.0'
+ ruby: '3.1'
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Set DB Adapter
+ env:
+ DB_ADAPTER: ${{ matrix.adapter }}
+
+ # See: https://github.com/actions/virtual-environments/blob/main/images/linux/Ubuntu2004-README.md#mysql
+ run: |
+ if [[ "${DB_ADAPTER}" == "mysql2" ]] || [[ "${DB_ADAPTER}" == "trilogy" ]]; then
+ sudo systemctl start mysql.service
+ mysql -u root -proot -e 'create database ajax_datatables_rails;'
+ fi
+
+ - name: Setup Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: ${{ matrix.ruby }}
+ bundler-cache: true
+ env:
+ DB_ADAPTER: ${{ matrix.adapter }}
+
+ - name: Run RSpec
+ env:
+ DB_ADAPTER: ${{ matrix.adapter }}
+ run: bin/rspec
+
+ - name: Publish code coverage
+ uses: qltysh/qlty-action/coverage@v1
+ with:
+ token: ${{ secrets.QLTY_COVERAGE_TOKEN }}
+ files: coverage/coverage.json
diff --git a/.github/workflows/ci_oracle.yml b/.github/workflows/ci_oracle.yml
new file mode 100644
index 00000000..7df46ebf
--- /dev/null
+++ b/.github/workflows/ci_oracle.yml
@@ -0,0 +1,104 @@
+---
+name: CI Oracle
+
+on:
+ push:
+ branches:
+ - '**'
+ pull_request:
+ branches:
+ - '**'
+ schedule:
+ - cron: '0 4 1 * *'
+ # Run workflow manually
+ workflow_dispatch:
+
+jobs:
+ rspec:
+ runs-on: ubuntu-latest
+
+ env: # $BUNDLE_GEMFILE must be set at the job level, so it is set for all steps
+ BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/${{ matrix.rails }}_with_${{ matrix.adapter }}.gemfile
+ ORACLE_HOME: /opt/oracle/instantclient_23_8
+ LD_LIBRARY_PATH: /opt/oracle/instantclient_23_8
+ TNS_ADMIN: ./ci/network/admin
+ DATABASE_SYS_PASSWORD: Oracle18
+ DATABASE_NAME: FREEPDB1
+
+ services:
+ oracle:
+ image: gvenzl/oracle-free:latest
+ ports:
+ - 1521:1521
+ env:
+ TZ: Europe/Paris
+ ORACLE_PASSWORD: Oracle18
+ options: >-
+ --health-cmd healthcheck.sh
+ --health-interval 10s
+ --health-timeout 5s
+ --health-retries 10
+
+ strategy:
+ fail-fast: false
+ matrix:
+ ruby:
+ - '3.4'
+ - '3.3'
+ - '3.2'
+ - '3.1'
+ - 'head'
+ rails:
+ - rails_8.0
+ - rails_7.2
+ - rails_7.1
+ adapter:
+ - oracle_enhanced
+ exclude:
+ - rails: 'rails_8.0'
+ ruby: '3.1'
+ adapter: 'oracle_enhanced'
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Create symbolic link for libaio library compatibility
+ run: |
+ sudo ln -s /usr/lib/x86_64-linux-gnu/libaio.so.1t64 /usr/lib/x86_64-linux-gnu/libaio.so.1
+
+ - name: Download Oracle instant client
+ run: |
+ wget -q https://download.oracle.com/otn_software/linux/instantclient/2380000/instantclient-basic-linux.x64-23.8.0.25.04.zip
+ wget -q https://download.oracle.com/otn_software/linux/instantclient/2380000/instantclient-sdk-linux.x64-23.8.0.25.04.zip
+ wget -q https://download.oracle.com/otn_software/linux/instantclient/2380000/instantclient-sqlplus-linux.x64-23.8.0.25.04.zip
+
+ - name: Install Oracle instant client
+ run: |
+ sudo unzip instantclient-basic-linux.x64-23.8.0.25.04.zip -d /opt/oracle/
+ sudo unzip -o instantclient-sdk-linux.x64-23.8.0.25.04.zip -d /opt/oracle/
+ sudo unzip -o instantclient-sqlplus-linux.x64-23.8.0.25.04.zip -d /opt/oracle/
+ echo "/opt/oracle/instantclient_23_8" >> $GITHUB_PATH
+
+ - name: Create database user
+ run: |
+ ./ci/setup_accounts.sh
+
+ - name: Setup Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: ${{ matrix.ruby }}
+ bundler-cache: true
+ env:
+ DB_ADAPTER: ${{ matrix.adapter }}
+
+ - name: Run RSpec
+ env:
+ DB_ADAPTER: ${{ matrix.adapter }}
+ run: bin/rspec
+
+ - name: Publish code coverage
+ uses: qltysh/qlty-action/coverage@v1
+ with:
+ token: ${{ secrets.QLTY_COVERAGE_TOKEN }}
+ files: coverage/coverage.json
diff --git a/.gitignore b/.gitignore
index 431da6a1..bb537c9d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,11 +10,17 @@
/coverage
/tmp
-# Ignore sqlite db file
-/ajax_datatables_rails
-
# RVM files
/.ruby-version
# Gem files
/*.gem
+
+# Ignore dummy app files
+spec/dummy/db/*.sqlite3
+spec/dummy/db/*.sqlite3-journal
+spec/dummy/log/*.log
+spec/dummy/tmp/
+
+# Ignore MacOS files
+.DS_Store
diff --git a/.qlty/qlty.toml b/.qlty/qlty.toml
new file mode 100644
index 00000000..75811a42
--- /dev/null
+++ b/.qlty/qlty.toml
@@ -0,0 +1,35 @@
+config_version = "0"
+
+[[source]]
+name = "default"
+default = true
+
+[[plugin]]
+name = "actionlint"
+
+[[plugin]]
+name = "checkov"
+version = "3.2.49"
+
+[[plugin]]
+name = "markdownlint"
+version = "0.31.1"
+
+[[plugin]]
+name = "osv-scanner"
+
+[[plugin]]
+name = "prettier"
+version = "2.8.4"
+
+[[plugin]]
+name = "ripgrep"
+
+[[plugin]]
+name = "trivy"
+
+[[plugin]]
+name = "trufflehog"
+
+[[plugin]]
+name = "yamllint"
diff --git a/.rspec b/.rspec
index 4e1e0d2f..372b5acf 100644
--- a/.rspec
+++ b/.rspec
@@ -1 +1 @@
---color
+--warnings
diff --git a/.rubocop.yml b/.rubocop.yml
index 631862d4..63074818 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -1,1157 +1,85 @@
-AllCops:
- DisabledByDefault: true
-
-#################### Lint ################################
-
-Lint/AmbiguousOperator:
- Description: >-
- Checks for ambiguous operators in the first argument of a
- method invocation without parentheses.
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#parens-as-args'
- Enabled: true
-
-Lint/AmbiguousRegexpLiteral:
- Description: >-
- Checks for ambiguous regexp literals in the first argument of
- a method invocation without parenthesis.
- Enabled: true
-
-Lint/AssignmentInCondition:
- Description: "Don't use assignment in conditions."
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#safe-assignment-in-condition'
- Enabled: true
-
-Lint/BlockAlignment:
- Description: 'Align block ends correctly.'
- Enabled: true
-
-Lint/CircularArgumentReference:
- Description: "Don't refer to the keyword argument in the default value."
- Enabled: true
-
-Lint/ConditionPosition:
- Description: >-
- Checks for condition placed in a confusing position relative to
- the keyword.
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#same-line-condition'
- Enabled: true
-
-Lint/Debugger:
- Description: 'Check for debugger calls.'
- Enabled: true
-
-Lint/DefEndAlignment:
- Description: 'Align ends corresponding to defs correctly.'
- Enabled: true
-
-Lint/DeprecatedClassMethods:
- Description: 'Check for deprecated class method calls.'
- Enabled: true
-
-Lint/DuplicateMethods:
- Description: 'Check for duplicate methods calls.'
- Enabled: true
-
-Lint/EachWithObjectArgument:
- Description: 'Check for immutable argument given to each_with_object.'
- Enabled: true
-
-Lint/ElseLayout:
- Description: 'Check for odd code arrangement in an else block.'
- Enabled: true
-
-Lint/EmptyEnsure:
- Description: 'Checks for empty ensure block.'
- Enabled: true
-
-Lint/EmptyInterpolation:
- Description: 'Checks for empty string interpolation.'
- Enabled: true
-
-Lint/EndAlignment:
- Description: 'Align ends correctly.'
- Enabled: true
-
-Lint/EndInMethod:
- Description: 'END blocks should not be placed inside method definitions.'
- Enabled: true
-
-Lint/EnsureReturn:
- Description: 'Do not use return in an ensure block.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-return-ensure'
- Enabled: true
-
-Lint/Eval:
- Description: 'The use of eval represents a serious security risk.'
- Enabled: true
-
-Lint/FormatParameterMismatch:
- Description: 'The number of parameters to format/sprint must match the fields.'
- Enabled: true
-
-Lint/HandleExceptions:
- Description: "Don't suppress exception."
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#dont-hide-exceptions'
- Enabled: true
-
-Lint/InvalidCharacterLiteral:
- Description: >-
- Checks for invalid character literals with a non-escaped
- whitespace character.
- Enabled: true
-
-Lint/LiteralInCondition:
- Description: 'Checks of literals used in conditions.'
- Enabled: true
-
-Lint/LiteralInInterpolation:
- Description: 'Checks for literals used in interpolation.'
- Enabled: true
-
-Lint/Loop:
- Description: >-
- Use Kernel#loop with break rather than begin/end/until or
- begin/end/while for post-loop tests.
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#loop-with-break'
- Enabled: true
-
-Lint/NestedMethodDefinition:
- Description: 'Do not use nested method definitions.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-nested-methods'
- Enabled: true
-
-Lint/NonLocalExitFromIterator:
- Description: 'Do not use return in iterator to cause non-local exit.'
- Enabled: true
-
-Lint/ParenthesesAsGroupedExpression:
- Description: >-
- Checks for method calls with a space before the opening
- parenthesis.
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#parens-no-spaces'
- Enabled: true
-
-Lint/RequireParentheses:
- Description: >-
- Use parentheses in the method call to avoid confusion
- about precedence.
- Enabled: true
-
-Lint/RescueException:
- Description: 'Avoid rescuing the Exception class.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-blind-rescues'
- Enabled: true
-
-Lint/ShadowingOuterLocalVariable:
- Description: >-
- Do not use the same name as outer local variable
- for block arguments or block local variables.
- Enabled: true
-
-Lint/StringConversionInInterpolation:
- Description: 'Checks for Object#to_s usage in string interpolation.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-to-s'
- Enabled: true
-
-Lint/UnderscorePrefixedVariableName:
- Description: 'Do not use prefix `_` for a variable that is used.'
- Enabled: true
-
-Lint/UnneededDisable:
- Description: >-
- Checks for rubocop:disable comments that can be removed.
- Note: this cop is not disabled when disabling all cops.
- It must be explicitly disabled.
- Enabled: true
-
-Lint/UnusedBlockArgument:
- Description: 'Checks for unused block arguments.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#underscore-unused-vars'
- Enabled: true
-
-Lint/UnusedMethodArgument:
- Description: 'Checks for unused method arguments.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#underscore-unused-vars'
- Enabled: true
-
-Lint/UnreachableCode:
- Description: 'Unreachable code.'
- Enabled: true
-
-Lint/UselessAccessModifier:
- Description: 'Checks for useless access modifiers.'
- Enabled: true
-
-Lint/UselessAssignment:
- Description: 'Checks for useless assignment to a local variable.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#underscore-unused-vars'
- Enabled: true
-
-Lint/UselessComparison:
- Description: 'Checks for comparison of something with itself.'
- Enabled: true
-
-Lint/UselessElseWithoutRescue:
- Description: 'Checks for useless `else` in `begin..end` without `rescue`.'
- Enabled: true
-
-Lint/UselessSetterCall:
- Description: 'Checks for useless setter call to a local variable.'
- Enabled: true
-
-Lint/Void:
- Description: 'Possible use of operator/literal/variable in void context.'
- Enabled: true
-
-###################### Metrics ####################################
-
-Metrics/AbcSize:
- Description: >-
- A calculated magnitude based on number of assignments,
- branches, and conditions.
- Reference: 'http://c2.com/cgi/wiki?AbcMetric'
- Enabled: false
- Max: 20
-
-Metrics/BlockNesting:
- Description: 'Avoid excessive block nesting'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#three-is-the-number-thou-shalt-count'
- Enabled: true
- Max: 4
-
-Metrics/ClassLength:
- Description: 'Avoid classes longer than 250 lines of code.'
- Enabled: true
- Max: 250
-
-Metrics/CyclomaticComplexity:
- Description: >-
- A complexity metric that is strongly correlated to the number
- of test cases needed to validate a method.
- Enabled: true
- Max: 7
-
-Metrics/LineLength:
- Description: 'Limit lines to 80 characters.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#80-character-limits'
- Enabled: false
-
-Metrics/MethodLength:
- Description: 'Avoid methods longer than 30 lines of code.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#short-methods'
- Enabled: true
- Max: 30
-
-Metrics/ModuleLength:
- Description: 'Avoid modules longer than 250 lines of code.'
- Enabled: true
- Max: 250
-
-Metrics/ParameterLists:
- Description: 'Avoid parameter lists longer than three or four parameters.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#too-many-params'
- Enabled: true
-
-Metrics/PerceivedComplexity:
- Description: >-
- A complexity metric geared towards measuring complexity for a
- human reader.
- Enabled: false
-
-##################### Performance #############################
-
-Performance/Count:
- Description: >-
- Use `count` instead of `select...size`, `reject...size`,
- `select...count`, `reject...count`, `select...length`,
- and `reject...length`.
- Enabled: true
-
-Performance/Detect:
- Description: >-
- Use `detect` instead of `select.first`, `find_all.first`,
- `select.last`, and `find_all.last`.
- Reference: 'https://github.com/JuanitoFatas/fast-ruby#enumerabledetect-vs-enumerableselectfirst-code'
- Enabled: true
-
-Performance/FlatMap:
- Description: >-
- Use `Enumerable#flat_map`
- instead of `Enumerable#map...Array#flatten(1)`
- or `Enumberable#collect..Array#flatten(1)`
- Reference: 'https://github.com/JuanitoFatas/fast-ruby#enumerablemaparrayflatten-vs-enumerableflat_map-code'
- Enabled: true
- EnabledForFlattenWithoutParams: false
- # If enabled, this cop will warn about usages of
- # `flatten` being called without any parameters.
- # This can be dangerous since `flat_map` will only flatten 1 level, and
- # `flatten` without any parameters can flatten multiple levels.
-
-Performance/ReverseEach:
- Description: 'Use `reverse_each` instead of `reverse.each`.'
- Reference: 'https://github.com/JuanitoFatas/fast-ruby#enumerablereverseeach-vs-enumerablereverse_each-code'
- Enabled: true
-
-Performance/Sample:
- Description: >-
- Use `sample` instead of `shuffle.first`,
- `shuffle.last`, and `shuffle[Fixnum]`.
- Reference: 'https://github.com/JuanitoFatas/fast-ruby#arrayshufflefirst-vs-arraysample-code'
- Enabled: true
-
-Performance/Size:
- Description: >-
- Use `size` instead of `count` for counting
- the number of elements in `Array` and `Hash`.
- Reference: 'https://github.com/JuanitoFatas/fast-ruby#arraycount-vs-arraysize-code'
- Enabled: true
-
-Performance/StringReplacement:
- Description: >-
- Use `tr` instead of `gsub` when you are replacing the same
- number of characters. Use `delete` instead of `gsub` when
- you are deleting characters.
- Reference: 'https://github.com/JuanitoFatas/fast-ruby#stringgsub-vs-stringtr-code'
- Enabled: true
-
-##################### Rails ##################################
-
-Rails/ActionFilter:
- Description: 'Enforces consistent use of action filter methods.'
- Enabled: false
-
-Rails/Date:
- Description: >-
- Checks the correct usage of date aware methods,
- such as Date.today, Date.current etc.
- Enabled: false
-
-Rails/Delegate:
- Description: 'Prefer delegate method for delegations.'
- Enabled: false
-
-Rails/FindBy:
- Description: 'Prefer find_by over where.first.'
- Enabled: false
-
-Rails/FindEach:
- Description: 'Prefer all.find_each over all.find.'
- Enabled: false
-
-Rails/HasAndBelongsToMany:
- Description: 'Prefer has_many :through to has_and_belongs_to_many.'
- Enabled: false
-
-Rails/Output:
- Description: 'Checks for calls to puts, print, etc.'
- Enabled: false
-
-Rails/ReadWriteAttribute:
- Description: >-
- Checks for read_attribute(:attr) and
- write_attribute(:attr, val).
- Enabled: false
-
-Rails/ScopeArgs:
- Description: 'Checks the arguments of ActiveRecord scopes.'
- Enabled: false
-
-Rails/TimeZone:
- Description: 'Checks the correct usage of time zone aware methods.'
- StyleGuide: 'https://github.com/bbatsov/rails-style-guide#time'
- Reference: 'http://danilenko.org/2012/7/6/rails_timezones'
- Enabled: false
-
-Rails/Validation:
- Description: 'Use validates :attribute, hash of validations.'
- Enabled: false
-
-################## Style #################################
-
-Style/AccessModifierIndentation:
- Description: Check indentation of private/protected visibility modifiers.
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#indent-public-private-protected'
- Enabled: false
-
-Style/AccessorMethodName:
- Description: Check the naming of accessor methods for get_/set_.
- Enabled: false
-
-Style/Alias:
- Description: 'Use alias_method instead of alias.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#alias-method'
- Enabled: false
-
-Style/AlignArray:
- Description: >-
- Align the elements of an array literal if they span more than
- one line.
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#align-multiline-arrays'
- Enabled: false
-
-Style/AlignHash:
- Description: >-
- Align the elements of a hash literal if they span more than
- one line.
- Enabled: false
-
-Style/AlignParameters:
- Description: >-
- Align the parameters of a method call if they span more
- than one line.
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-double-indent'
- Enabled: false
-
-Style/AndOr:
- Description: 'Use &&/|| instead of and/or.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-and-or-or'
- Enabled: false
-
-Style/ArrayJoin:
- Description: 'Use Array#join instead of Array#*.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#array-join'
- Enabled: false
-
-Style/AsciiComments:
- Description: 'Use only ascii symbols in comments.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#english-comments'
- Enabled: false
-
-Style/AsciiIdentifiers:
- Description: 'Use only ascii symbols in identifiers.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#english-identifiers'
- Enabled: false
-
-Style/Attr:
- Description: 'Checks for uses of Module#attr.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#attr'
- Enabled: false
-
-Style/BeginBlock:
- Description: 'Avoid the use of BEGIN blocks.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-BEGIN-blocks'
- Enabled: false
+---
+plugins:
+ - rubocop-factory_bot
+ - rubocop-performance
+ - rubocop-rake
+ - rubocop-rspec
-Style/BarePercentLiterals:
- Description: 'Checks if usage of %() or %Q() matches configuration.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-q-shorthand'
- Enabled: false
-
-Style/BlockComments:
- Description: 'Do not use block comments.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-block-comments'
- Enabled: false
-
-Style/BlockEndNewline:
- Description: 'Put end statement of multiline block on its own line.'
- Enabled: false
-
-Style/BlockDelimiters:
- Description: >-
- Avoid using {...} for multi-line blocks (multiline chaining is
- always ugly).
- Prefer {...} over do...end for single-line blocks.
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#single-line-blocks'
- Enabled: false
-
-Style/BracesAroundHashParameters:
- Description: 'Enforce braces style around hash parameters.'
- Enabled: false
-
-Style/CaseEquality:
- Description: 'Avoid explicit use of the case equality operator(===).'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-case-equality'
- Enabled: false
-
-Style/CaseIndentation:
- Description: 'Indentation of when in a case/when/[else/]end.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#indent-when-to-case'
- Enabled: false
-
-Style/CharacterLiteral:
- Description: 'Checks for uses of character literals.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-character-literals'
- Enabled: false
-
-Style/ClassAndModuleCamelCase:
- Description: 'Use CamelCase for classes and modules.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#camelcase-classes'
- Enabled: false
-
-Style/ClassAndModuleChildren:
- Description: 'Checks style of children classes and modules.'
- Enabled: false
-
-Style/ClassCheck:
- Description: 'Enforces consistent use of `Object#is_a?` or `Object#kind_of?`.'
- Enabled: false
-
-Style/ClassMethods:
- Description: 'Use self when defining module/class methods.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#def-self-class-methods'
- Enabled: false
-
-Style/ClassVars:
- Description: 'Avoid the use of class variables.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-class-vars'
- Enabled: false
-
-Style/ClosingParenthesisIndentation:
- Description: 'Checks the indentation of hanging closing parentheses.'
- Enabled: false
-
-Style/ColonMethodCall:
- Description: 'Do not use :: for method call.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#double-colons'
- Enabled: false
-
-Style/CommandLiteral:
- Description: 'Use `` or %x around command literals.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-x'
- Enabled: false
-
-Style/CommentAnnotation:
- Description: 'Checks formatting of annotation comments.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#annotate-keywords'
- Enabled: false
-
-Style/CommentIndentation:
- Description: 'Indentation of comments.'
- Enabled: false
-
-Style/ConstantName:
- Description: 'Constants should use SCREAMING_SNAKE_CASE.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#screaming-snake-case'
- Enabled: false
-
-Style/DefWithParentheses:
- Description: 'Use def with parentheses when there are arguments.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#method-parens'
- Enabled: false
+AllCops:
+ NewCops: enable
+ TargetRubyVersion: 3.1
+ Exclude:
+ - bin/*
+ - gemfiles/*
+ - spec/dummy/**/*
-Style/PreferredHashMethods:
- Description: 'Checks for use of deprecated Hash methods.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#hash-key'
- Enabled: false
+#########
+# STYLE #
+#########
Style/Documentation:
- Description: 'Document classes and non-namespace modules.'
- Enabled: false
-
-Style/DotPosition:
- Description: 'Checks the position of the dot in multi-line method calls.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#consistent-multi-line-chains'
- Enabled: false
-
-Style/DoubleNegation:
- Description: 'Checks for uses of double negation (!!).'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-bang-bang'
- Enabled: false
-
-Style/EachWithObject:
- Description: 'Prefer `each_with_object` over `inject` or `reduce`.'
- Enabled: false
-
-Style/ElseAlignment:
- Description: 'Align elses and elsifs correctly.'
- Enabled: false
-
-Style/EmptyElse:
- Description: 'Avoid empty else-clauses.'
- Enabled: false
-
-Style/EmptyLineBetweenDefs:
- Description: 'Use empty lines between defs.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#empty-lines-between-methods'
- Enabled: false
-
-Style/EmptyLines:
- Description: "Don't use several empty lines in a row."
- Enabled: false
-
-Style/EmptyLinesAroundAccessModifier:
- Description: "Keep blank lines around access modifiers."
Enabled: false
-Style/EmptyLinesAroundBlockBody:
- Description: "Keeps track of empty lines around block bodies."
- Enabled: false
-
-Style/EmptyLinesAroundClassBody:
- Description: "Keeps track of empty lines around class bodies."
- Enabled: false
+Style/TrailingCommaInArrayLiteral:
+ EnforcedStyleForMultiline: comma
-Style/EmptyLinesAroundModuleBody:
- Description: "Keeps track of empty lines around module bodies."
- Enabled: false
-
-Style/EmptyLinesAroundMethodBody:
- Description: "Keeps track of empty lines around method bodies."
- Enabled: false
-
-Style/EmptyLiteral:
- Description: 'Prefer literals to Array.new/Hash.new/String.new.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#literal-array-hash'
- Enabled: false
-
-Style/EndBlock:
- Description: 'Avoid the use of END blocks.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-END-blocks'
- Enabled: false
-
-Style/EndOfLine:
- Description: 'Use Unix-style line endings.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#crlf'
- Enabled: false
-
-Style/EvenOdd:
- Description: 'Favor the use of Fixnum#even? && Fixnum#odd?'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#predicate-methods'
- Enabled: false
+Style/TrailingCommaInHashLiteral:
+ EnforcedStyleForMultiline: comma
-Style/ExtraSpacing:
- Description: 'Do not use unnecessary spacing.'
- Enabled: false
-
-Style/FileName:
- Description: 'Use snake_case for source file names.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#snake-case-files'
- Enabled: false
-
-Style/InitialIndentation:
- Description: >-
- Checks the indentation of the first non-blank non-comment line in a file.
- Enabled: false
-
-Style/FirstParameterIndentation:
- Description: 'Checks the indentation of the first parameter in a method call.'
- Enabled: false
-
-Style/FlipFlop:
- Description: 'Checks for flip flops'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-flip-flops'
- Enabled: false
-
-Style/For:
- Description: 'Checks use of for or each in multiline loops.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-for-loops'
- Enabled: false
-
-Style/FormatString:
- Description: 'Enforce the use of Kernel#sprintf, Kernel#format or String#%.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#sprintf'
- Enabled: false
-
-Style/GlobalVars:
- Description: 'Do not introduce global variables.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#instance-vars'
- Reference: 'http://www.zenspider.com/Languages/Ruby/QuickRef.html'
- Enabled: false
-
-Style/GuardClause:
- Description: 'Check for conditionals that can be replaced with guard clauses'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-nested-conditionals'
- Enabled: false
-
-Style/HashSyntax:
- Description: >-
- Prefer Ruby 1.9 hash syntax { a: 1, b: 2 } over 1.8 syntax
- { :a => 1, :b => 2 }.
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#hash-literals'
- Enabled: false
-
-Style/IfUnlessModifier:
- Description: >-
- Favor modifier if/unless usage when you have a
- single-line body.
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#if-as-a-modifier'
- Enabled: false
-
-Style/IfWithSemicolon:
- Description: 'Do not use if x; .... Use the ternary operator instead.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-semicolon-ifs'
- Enabled: false
-
-Style/IndentationConsistency:
- Description: 'Keep indentation straight.'
- Enabled: false
-
-Style/IndentationWidth:
- Description: 'Use 2 spaces for indentation.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-indentation'
- Enabled: false
-
-Style/IndentArray:
- Description: >-
- Checks the indentation of the first element in an array
- literal.
- Enabled: false
-
-Style/IndentHash:
- Description: 'Checks the indentation of the first key in a hash literal.'
- Enabled: false
-
-Style/InfiniteLoop:
- Description: 'Use Kernel#loop for infinite loops.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#infinite-loop'
- Enabled: false
-
-Style/Lambda:
- Description: 'Use the new lambda literal syntax for single-line blocks.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#lambda-multi-line'
- Enabled: false
-
-Style/LambdaCall:
- Description: 'Use lambda.call(...) instead of lambda.(...).'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#proc-call'
- Enabled: false
-
-Style/LeadingCommentSpace:
- Description: 'Comments should start with a space.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#hash-space'
- Enabled: false
-
-Style/LineEndConcatenation:
- Description: >-
- Use \ instead of + or << to concatenate two string literals at
- line end.
- Enabled: false
-
-Style/MethodCallParentheses:
- Description: 'Do not use parentheses for method calls with no arguments.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-args-no-parens'
- Enabled: false
-
-Style/MethodDefParentheses:
- Description: >-
- Checks if the method definitions have or don't have
- parentheses.
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#method-parens'
- Enabled: false
-
-Style/MethodName:
- Description: 'Use the configured style when naming methods.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#snake-case-symbols-methods-vars'
- Enabled: false
-
-Style/ModuleFunction:
- Description: 'Checks for usage of `extend self` in modules.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#module-function'
- Enabled: false
-
-Style/MultilineBlockChain:
- Description: 'Avoid multi-line chains of blocks.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#single-line-blocks'
- Enabled: false
-
-Style/MultilineBlockLayout:
- Description: 'Ensures newlines after multiline block do statements.'
- Enabled: false
-
-Style/MultilineIfThen:
- Description: 'Do not use then for multi-line if/unless.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-then'
- Enabled: false
-
-Style/MultilineOperationIndentation:
- Description: >-
- Checks indentation of binary operations that span more than
- one line.
- Enabled: false
-
-Style/MultilineTernaryOperator:
- Description: >-
- Avoid multi-line ?: (the ternary operator);
- use if/unless instead.
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-multiline-ternary'
- Enabled: false
-
-Style/NegatedIf:
- Description: >-
- Favor unless over if for negative conditions
- (or control flow or).
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#unless-for-negatives'
- Enabled: false
-
-Style/NegatedWhile:
- Description: 'Favor until over while for negative conditions.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#until-for-negatives'
- Enabled: false
-
-Style/NestedTernaryOperator:
- Description: 'Use one expression per branch in a ternary operator.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-nested-ternary'
- Enabled: false
-
-Style/Next:
- Description: 'Use `next` to skip iteration instead of a condition at the end.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-nested-conditionals'
- Enabled: false
-
-Style/NilComparison:
- Description: 'Prefer x.nil? to x == nil.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#predicate-methods'
- Enabled: false
-
-Style/NonNilCheck:
- Description: 'Checks for redundant nil checks.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-non-nil-checks'
- Enabled: false
-
-Style/Not:
- Description: 'Use ! instead of not.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#bang-not-not'
- Enabled: false
-
-Style/NumericLiterals:
- Description: >-
- Add underscores to large numeric literals to improve their
- readability.
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#underscores-in-numerics'
- Enabled: false
-
-Style/OneLineConditional:
- Description: >-
- Favor the ternary operator(?:) over
- if/then/else/end constructs.
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#ternary-operator'
- Enabled: false
-
-Style/OpMethod:
- Description: 'When defining binary operators, name the argument other.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#other-arg'
- Enabled: false
-
-Style/OptionalArguments:
- Description: >-
- Checks for optional arguments that do not appear at the end
- of the argument list
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#optional-arguments'
- Enabled: false
-
-Style/ParallelAssignment:
- Description: >-
- Check for simple usages of parallel assignment.
- It will only warn when the number of variables
- matches on both sides of the assignment.
- This also provides performance benefits
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#parallel-assignment'
- Enabled: false
-
-Style/ParenthesesAroundCondition:
- Description: >-
- Don't use parentheses around the condition of an
- if/unless/while.
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-parens-if'
- Enabled: false
-
-Style/PercentLiteralDelimiters:
- Description: 'Use `%`-literal delimiters consistently'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-literal-braces'
- Enabled: false
-
-Style/PercentQLiterals:
- Description: 'Checks if uses of %Q/%q match the configured preference.'
- Enabled: false
-
-Style/PerlBackrefs:
- Description: 'Avoid Perl-style regex back references.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-perl-regexp-last-matchers'
- Enabled: false
-
-Style/PredicateName:
- Description: 'Check the names of predicate methods.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#bool-methods-qmark'
- Enabled: false
-
-Style/Proc:
- Description: 'Use proc instead of Proc.new.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#proc'
- Enabled: false
-
-Style/RaiseArgs:
- Description: 'Checks the arguments passed to raise/fail.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#exception-class-messages'
- Enabled: false
-
-Style/RedundantBegin:
- Description: "Don't use begin blocks when they are not needed."
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#begin-implicit'
- Enabled: false
-
-Style/RedundantException:
- Description: "Checks for an obsolete RuntimeException argument in raise/fail."
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-explicit-runtimeerror'
- Enabled: false
-
-Style/RedundantReturn:
- Description: "Don't use return where it's not required."
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-explicit-return'
- Enabled: false
-
-Style/RedundantSelf:
- Description: "Don't use self where it's not needed."
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-self-unless-required'
- Enabled: false
-
-Style/RegexpLiteral:
- Description: 'Use / or %r around regular expressions.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-r'
- Enabled: false
-
-Style/RescueEnsureAlignment:
- Description: 'Align rescues and ensures correctly.'
- Enabled: false
-
-Style/RescueModifier:
- Description: 'Avoid using rescue in its modifier form.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-rescue-modifiers'
- Enabled: false
-
-Style/SelfAssignment:
- Description: >-
- Checks for places where self-assignment shorthand should have
- been used.
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#self-assignment'
- Enabled: false
-
-Style/Semicolon:
- Description: "Don't use semicolons to terminate expressions."
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-semicolon'
- Enabled: false
-
-Style/SignalException:
- Description: 'Checks for proper usage of fail and raise.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#fail-method'
- Enabled: false
-
-Style/SingleLineBlockParams:
- Description: 'Enforces the names of some block params.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#reduce-blocks'
- Enabled: false
-
-Style/SingleLineMethods:
- Description: 'Avoid single-line methods.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-single-line-methods'
- Enabled: false
-
-Style/SpaceBeforeFirstArg:
- Description: >-
- Checks that exactly one space is used between a method name
- and the first argument for method calls without parentheses.
- Enabled: true
-
-Style/SpaceAfterColon:
- Description: 'Use spaces after colons.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-operators'
- Enabled: false
-
-Style/SpaceAfterComma:
- Description: 'Use spaces after commas.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-operators'
- Enabled: false
-
-Style/SpaceAroundKeyword:
- Description: 'Use spaces around keywords.'
- Enabled: false
-
-Style/SpaceAfterMethodName:
- Description: >-
- Do not put a space between a method name and the opening
- parenthesis in a method definition.
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#parens-no-spaces'
- Enabled: false
-
-Style/SpaceAfterNot:
- Description: Tracks redundant space after the ! operator.
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-space-bang'
- Enabled: false
-
-Style/SpaceAfterSemicolon:
- Description: 'Use spaces after semicolons.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-operators'
- Enabled: false
-
-Style/SpaceBeforeBlockBraces:
- Description: >-
- Checks that the left block brace has or doesn't have space
- before it.
- Enabled: false
-
-Style/SpaceBeforeComma:
- Description: 'No spaces before commas.'
- Enabled: false
-
-Style/SpaceBeforeComment:
- Description: >-
- Checks for missing space between code and a comment on the
- same line.
- Enabled: false
-
-Style/SpaceBeforeSemicolon:
- Description: 'No spaces before semicolons.'
- Enabled: false
-
-Style/SpaceInsideBlockBraces:
- Description: >-
- Checks that block braces have or don't have surrounding space.
- For blocks taking parameters, checks that the left brace has
- or doesn't have trailing space.
- Enabled: false
-
-Style/SpaceAroundBlockParameters:
- Description: 'Checks the spacing inside and after block parameters pipes.'
- Enabled: false
-
-Style/SpaceAroundEqualsInParameterDefault:
- Description: >-
- Checks that the equals signs in parameter default assignments
- have or don't have surrounding space depending on
- configuration.
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-around-equals'
- Enabled: false
-
-Style/SpaceAroundOperators:
- Description: 'Use a single space around operators.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-operators'
- Enabled: false
-
-Style/SpaceInsideBrackets:
- Description: 'No spaces after [ or before ].'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-spaces-braces'
- Enabled: false
-
-Style/SpaceInsideHashLiteralBraces:
- Description: "Use spaces inside hash literal braces - or don't."
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-operators'
- Enabled: false
-
-Style/SpaceInsideParens:
- Description: 'No spaces after ( or before ).'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-spaces-braces'
- Enabled: false
-
-Style/SpaceInsideRangeLiteral:
- Description: 'No spaces inside range literals.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-space-inside-range-literals'
- Enabled: false
-
-Style/SpaceInsideStringInterpolation:
- Description: 'Checks for padding/surrounding spaces inside string interpolation.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#string-interpolation'
- Enabled: false
-
-Style/SpecialGlobalVars:
- Description: 'Avoid Perl-style global variables.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-cryptic-perlisms'
- Enabled: false
-
-Style/StringLiterals:
- Description: 'Checks if uses of quotes match the configured preference.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#consistent-string-literals'
- Enabled: false
-
-Style/StringLiteralsInInterpolation:
- Description: >-
- Checks if uses of quotes inside expressions in interpolated
- strings match the configured preference.
- Enabled: false
+Style/BlockDelimiters:
+ AllowedPatterns: ['expect']
-Style/StructInheritance:
- Description: 'Checks for inheritance from Struct.new.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-extend-struct-new'
- Enabled: false
+##########
+# LAYOUT #
+##########
-Style/SymbolLiteral:
- Description: 'Use plain symbols instead of string symbols when possible.'
- Enabled: false
+Layout/LineLength:
+ Max: 150
+ Exclude:
+ - ajax-datatables-rails.gemspec
-Style/SymbolProc:
- Description: 'Use symbols as procs instead of blocks when possible.'
+Layout/EmptyLines:
Enabled: false
-Style/Tab:
- Description: 'No hard tabs.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-indentation'
+Layout/EmptyLineBetweenDefs:
Enabled: false
-Style/TrailingBlankLines:
- Description: 'Checks trailing blank lines and final newline.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#newline-eof'
+Layout/EmptyLinesAroundClassBody:
Enabled: false
-Style/TrailingCommaInArguments:
- Description: 'Checks for trailing comma in parameter lists.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-trailing-params-comma'
+Layout/EmptyLinesAroundBlockBody:
Enabled: false
-Style/TrailingCommaInLiteral:
- Description: 'Checks for trailing comma in literals.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-trailing-array-commas'
+Layout/EmptyLinesAroundModuleBody:
Enabled: false
-Style/TrailingWhitespace:
- Description: 'Avoid trailing whitespace.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-trailing-whitespace'
- Enabled: false
+Layout/HashAlignment:
+ EnforcedColonStyle: table
+ EnforcedHashRocketStyle: table
-Style/TrivialAccessors:
- Description: 'Prefer attr_* methods to trivial readers/writers.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#attr_family'
- Enabled: false
+##########
+# NAMING #
+##########
-Style/UnlessElse:
- Description: >-
- Do not use unless with else. Rewrite these with the positive
- case first.
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-else-with-unless'
- Enabled: false
+Naming/FileName:
+ Exclude:
+ - lib/ajax-datatables-rails.rb
-Style/UnneededCapitalW:
- Description: 'Checks for %W when interpolation is not needed.'
- Enabled: false
+#########
+# RSPEC #
+#########
-Style/UnneededPercentQ:
- Description: 'Checks for %q/%Q when single quotes or double quotes would do.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-q'
- Enabled: false
-
-Style/TrailingUnderscoreVariable:
- Description: >-
- Checks for the usage of unneeded trailing underscores at the
- end of parallel variable assignment.
- Enabled: false
-
-Style/VariableInterpolation:
- Description: >-
- Don't interpolate global, instance and class variables
- directly in strings.
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#curlies-interpolate'
- Enabled: false
-
-Style/VariableName:
- Description: 'Use the configured style when naming variables.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#snake-case-symbols-methods-vars'
- Enabled: false
+RSpec/MultipleExpectations:
+ Max: 7
-Style/WhenThen:
- Description: 'Use when x then ... for one-line cases.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#one-line-cases'
- Enabled: false
+RSpec/NestedGroups:
+ Max: 6
-Style/WhileUntilDo:
- Description: 'Checks for redundant do after while or until.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-multiline-while-do'
- Enabled: false
+RSpec/ExampleLength:
+ Max: 9
-Style/WhileUntilModifier:
- Description: >-
- Favor modifier while/until usage when you have a
- single-line body.
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#while-as-a-modifier'
- Enabled: false
+RSpec/MultipleMemoizedHelpers:
+ Max: 6
-Style/WordArray:
- Description: 'Use %w or %W for arrays of words.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-w'
- Enabled: false
+RSpec/NotToNot:
+ EnforcedStyle: to_not
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 05056a71..00000000
--- a/.travis.yml
+++ /dev/null
@@ -1,68 +0,0 @@
-dist: trusty
-language: ruby
-sudo: required
-cache: bundler
-rvm:
- - 2.2.7
- - 2.3.4
-gemfile:
- - gemfiles/rails_4.0.13.gemfile
- - gemfiles/rails_4.1.15.gemfile
- - gemfiles/rails_4.2.9.gemfile
- - gemfiles/rails_5.0.4.gemfile
- - gemfiles/rails_5.1.2.gemfile
-matrix:
- include:
- - rvm: 2.4.1
- gemfile: gemfiles/rails_4.2.9.gemfile
- env: DB_ADAPTER=postgresql
- - rvm: 2.4.1
- gemfile: gemfiles/rails_5.0.4.gemfile
- env: DB_ADAPTER=postgresql
- - rvm: 2.4.1
- gemfile: gemfiles/rails_5.1.2.gemfile
- env: DB_ADAPTER=postgresql
- - rvm: 2.4.1
- gemfile: gemfiles/rails_4.2.9.gemfile
- env: DB_ADAPTER=mysql2
- - rvm: 2.4.1
- gemfile: gemfiles/rails_5.0.4.gemfile
- env: DB_ADAPTER=mysql2
- - rvm: 2.4.1
- gemfile: gemfiles/rails_5.1.2.gemfile
- env: DB_ADAPTER=mysql2
- - rvm: 2.4.1
- gemfile: gemfiles/rails_4.2.9.gemfile
- env: DB_ADAPTER=oracle_enhanced
- - rvm: 2.4.1
- gemfile: gemfiles/rails_5.0.4.gemfile
- env: DB_ADAPTER=oracle_enhanced
- - rvm: 2.4.1
- gemfile: gemfiles/rails_5.1.2.gemfile
- env: DB_ADAPTER=oracle_enhanced
-after_success:
- - bundle exec codeclimate-test-reporter
-services:
- - postgresql
- - mysql
-addons:
- postgresql: '9.6'
- apt:
- packages:
- - mysql-server-5.6
- - mysql-client-core-5.6
- - mysql-client-5.6
-before_install:
- - sh -c "if [ '$DB_ADAPTER' = 'mysql2' ]; then mysql -e 'create database ajax_datatables_rails;'; fi"
- - sh -c "if [ '$DB_ADAPTER' = 'postgresql' ]; then psql -c 'create database ajax_datatables_rails;' -U postgres; fi"
- - sh -c "if [ '$DB_ADAPTER' = 'oracle_enhanced' ]; then ./spec/install_oracle.sh; fi"
-env:
- global:
- - ORACLE_COOKIE=sqldev
- - ORACLE_FILE=oracle11g/xe/oracle-xe-11.2.0-1.0.x86_64.rpm.zip
- - ORACLE_HOME=/u01/app/oracle/product/11.2.0/xe
- - ORACLE_SID=XE
- matrix:
- - DB_ADAPTER=postgresql
- - DB_ADAPTER=mysql2
- - DB_ADAPTER=oracle_enhanced
diff --git a/Appraisals b/Appraisals
index 56e6e7c7..d39912b0 100644
--- a/Appraisals
+++ b/Appraisals
@@ -1,34 +1,118 @@
-RAILS_VERSIONS = {
- '4.0.13' => {
- 'mysql2' => '~> 0.3.18',
- 'activerecord-oracle_enhanced-adapter' => '~> 1.5.0'
- },
- '4.1.15' => {
- 'mysql2' => '~> 0.3.18',
- 'activerecord-oracle_enhanced-adapter' => '~> 1.5.0'
- },
- '4.2.9' => {
- 'activerecord-oracle_enhanced-adapter' => '~> 1.6.0'
- },
- '5.0.4' => {
- 'activerecord-oracle_enhanced-adapter' => '~> 1.7.0',
- 'ruby-oci8' => ''
- },
- '5.1.2' => {
- 'activerecord-oracle_enhanced-adapter' => '~> 1.8.0',
- 'ruby-oci8' => ''
- }
-}
-
-RAILS_VERSIONS.each do |version, gems|
- appraise "rails_#{version}" do
- gem 'rails', version
- gems.each do |name, version|
- if version.empty?
- gem name
- else
- gem name, version
- end
- end
- end
+# frozen_string_literal: true
+
+###############
+# RAILS 7.1.0 #
+###############
+
+appraise 'rails_7.1_with_postgresql' do
+ gem 'rails', '~> 7.1.0'
+ gem 'pg'
+end
+
+appraise 'rails_7.1_with_sqlite3' do
+ gem 'rails', '~> 7.1.0'
+ gem 'sqlite3', '~> 1.5.0'
+ remove_gem 'pg'
+end
+
+appraise 'rails_7.1_with_mysql2' do
+ gem 'rails', '~> 7.1.0'
+ gem 'mysql2'
+ remove_gem 'pg'
+end
+
+appraise 'rails_7.1_with_trilogy' do
+ gem 'rails', '~> 7.1.0'
+ gem 'activerecord-trilogy-adapter'
+ remove_gem 'pg'
+end
+
+appraise 'rails_7.1_with_oracle_enhanced' do
+ gem 'rails', '~> 7.1.0'
+ gem 'activerecord-oracle_enhanced-adapter', '~> 7.1.0'
+ remove_gem 'pg'
+end
+
+appraise 'rails_7.1_with_postgis' do
+ gem 'rails', '~> 7.1.0'
+ gem 'pg'
+ gem 'activerecord-postgis-adapter', '~> 9.0.0'
+end
+
+###############
+# RAILS 7.2.0 #
+###############
+
+appraise 'rails_7.2_with_postgresql' do
+ gem 'rails', '~> 7.2.0'
+ gem 'pg'
+end
+
+appraise 'rails_7.2_with_sqlite3' do
+ gem 'rails', '~> 7.2.0'
+ gem 'sqlite3', '~> 1.5.0'
+ remove_gem 'pg'
+end
+
+appraise 'rails_7.2_with_mysql2' do
+ gem 'rails', '~> 7.2.0'
+ gem 'mysql2'
+ remove_gem 'pg'
+end
+
+appraise 'rails_7.2_with_trilogy' do
+ gem 'rails', '~> 7.2.0'
+ gem 'activerecord-trilogy-adapter'
+ remove_gem 'pg'
+end
+
+appraise 'rails_7.2_with_oracle_enhanced' do
+ gem 'rails', '~> 7.2.0'
+ gem 'activerecord-oracle_enhanced-adapter', '~> 7.2.0'
+ remove_gem 'pg'
+end
+
+appraise 'rails_7.2_with_postgis' do
+ gem 'rails', '~> 7.2.0'
+ gem 'pg'
+ gem 'activerecord-postgis-adapter', '~> 10.0.0'
+end
+
+###############
+# RAILS 8.0.0 #
+###############
+
+appraise 'rails_8.0_with_postgresql' do
+ gem 'rails', '~> 8.0.0'
+ gem 'pg'
+end
+
+appraise 'rails_8.0_with_sqlite3' do
+ gem 'rails', '~> 8.0.0'
+ gem 'sqlite3'
+ remove_gem 'pg'
+end
+
+appraise 'rails_8.0_with_mysql2' do
+ gem 'rails', '~> 8.0.0'
+ gem 'mysql2'
+ remove_gem 'pg'
+end
+
+appraise 'rails_8.0_with_trilogy' do
+ gem 'rails', '~> 8.0.0'
+ gem 'activerecord-trilogy-adapter'
+ remove_gem 'pg'
+end
+
+appraise 'rails_8.0_with_oracle_enhanced' do
+ gem 'rails', '~> 8.0.0'
+ gem 'activerecord-oracle_enhanced-adapter', '~> 8.0.0'
+ remove_gem 'pg'
+end
+
+appraise 'rails_8.0_with_postgis' do
+ gem 'rails', '~> 8.0.0'
+ gem 'pg'
+ gem 'activerecord-postgis-adapter', '~> 11.0.0'
end
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 91bc78a2..10407924 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,15 +1,129 @@
# CHANGELOG
-## 0.4.0
+## 1.6.0 (2025-??-??)
+
+* Remove dead code
+* Implementing `searchable: false` tests
+* Improve objects shape
+* Fix Rubocop offenses
+* Make gem smaller
+* Drop support of Rails 6.0
+* Drop support of Rails 6.1
+* Drop support of Rails 7.0
+* Drop support of Ruby 2.7
+* Drop support of Ruby 3.0
+* Add support for Rails 7.2
+* Add support for Rails 8.0
+* Add support for Ruby 3.4
+
+## 1.5.0 (2024-04-08)
+
+* Add support for grouped results (merge: [#419](https://github.com/jbox-web/ajax-datatables-rails/pull/419))
+* Fix server-side out of order ajax responses (merge: [#418](https://github.com/jbox-web/ajax-datatables-rails/pull/418))
+* Add support for postgis adapter (merge: [#417](https://github.com/jbox-web/ajax-datatables-rails/pull/417))
+* Add support for trilogy adapter (merge: [#423](https://github.com/jbox-web/ajax-datatables-rails/pull/423))
+* Drop support of Rails 5.2
+* Add support for Rails 7.1
+* Add support for Ruby 3.2
+* Add support for Ruby 3.3
+
+This is the last version to support Rails 6.0.x and Ruby 2.7.x.
+
+## 1.4.0 (2022-12-18)
+
+* Improve tests
+* Add tests on custom_field feature
+* Drop support of Ruby 2.5
+* Drop support of Ruby 2.6
+* Add support of Ruby 3.1
+* Add support of Rails 7.0
+* Fix: prevent establishing ActiveRecord connection on startup
+
+## 1.3.1 (2021-02-09)
+
+* Fix rare case error `uninitialized constant AjaxDatatablesRails::ActiveRecord::Base` (merge: [#379](https://github.com/jbox-web/ajax-datatables-rails/pull/379))
+
+## 1.3.0 (2021-01-04)
+
+* Drop support of Rails 5.0.x and 5.1.x
+* Drop support of Ruby 2.4
+* Add support of Rails 6.1
+* Add support of Ruby 3.0
+* Switch from Travis to Github Actions
+* Improve specs
+* Fix lib loading with JRuby (fixes [#371](https://github.com/jbox-web/ajax-datatables-rails/issues/371))
+* Raise an error when column's `cond:` setting is unknown
+* Make global search and column search work together (merge: [#350](https://github.com/jbox-web/ajax-datatables-rails/pull/350), fixes: [#258](https://github.com/jbox-web/ajax-datatables-rails/issues/258))
+* Fix: date_range doesn't support searching by a date greater than today (merge: [#351](https://github.com/jbox-web/ajax-datatables-rails/pull/351))
+* Fix: undefined method `fetch' for nil:NilClass (fix: [#307](https://github.com/jbox-web/ajax-datatables-rails/issues/307))
+* Add support for json params (merge: [#355](https://github.com/jbox-web/ajax-datatables-rails/pull/355))
+
+* `AjaxDatatablesRails.config` is removed with no replacement. The gem is now configless :)
+* `AjaxDatatablesRails.config.db_adapter=` is removed and is configured per datatable class now. It defaults to Rails DB adapter. (fixes [#364](https://github.com/jbox-web/ajax-datatables-rails/issues/364))
+* `AjaxDatatablesRails.config.nulls_last=` is removed and is configured per datatable class now (or by column). It defaults to false.
+
+To mitigate this 3 changes see the [migration doc](/doc/migrate.md).
+
+## 1.2.0 (2020-04-19)
+
+* Drop support of Rails 4.x
+* Drop support of Ruby 2.3
+* Use [zeitwerk](https://github.com/fxn/zeitwerk) to load gem files
+* Add binstubs to ease development
+
+This is the last version to support Rails 5.0.x, Rails 5.1.x and Ruby 2.4.x.
+
+## 1.1.0 (2019-12-12)
+
+* Add rudimentary support for Microsoft SQL Server
+* Fixes errors when options[param] is nil [PR 315](https://github.com/jbox-web/ajax-datatables-rails/pull/315) (thanks @allard)
+* Improve query performance when nulls_last option is enabled [PR 317](https://github.com/jbox-web/ajax-datatables-rails/pull/317) (thanks @natebird)
+* Add :string_in cond [PR 323](https://github.com/jbox-web/ajax-datatables-rails/pull/323) (thanks @donnguyen)
+* Rename `sanitize` private method [PR 326](https://github.com/jbox-web/ajax-datatables-rails/pull/326) (thanks @epipheus)
+* Update documentation
+* Test with latest Rails (6.x) and Ruby versions (2.6)
+
+This is the last version to support Rails 4.x and Ruby 2.3.x.
+
+## 1.0.0 (2018-08-28)
+
+* Breaking change: Remove dependency on view_context [Issue #288](https://github.com/jbox-web/ajax-datatables-rails/issues/288)
+* Breaking change: Replace `config.orm = :active_record` by a class : `AjaxDatatablesRails::ActiveRecord` [Fix #228](https://github.com/jbox-web/ajax-datatables-rails/issues/228)
+
+To mitigate this 2 changes see the [migration doc](/doc/migrate.md).
+
+## 0.4.3 (2018-06-05)
+
+* Add: Add `:string_eq` condition on columns filter [Issue #291](https://github.com/jbox-web/ajax-datatables-rails/issues/291)
+
+**Note :** This is the last version to support Rails 4.0.x and Rails 4.1.x
+
+## 0.4.2 (2018-05-15)
+
+* Fix: Integer out of range [PR #289](https://github.com/jbox-web/ajax-datatables-rails/pull/289) from [PR #284](https://github.com/jbox-web/ajax-datatables-rails/pull/284)
+
+## 0.4.1 (2018-05-06)
+
+* Fix: Restore behavior of #filter method [Comment](https://github.com/jbox-web/ajax-datatables-rails/commit/07795fd26849ff1b3b567f4ce967f722907a45be#comments)
+* Fix: Fix erroneous offset/start behavior [PR #264](https://github.com/jbox-web/ajax-datatables-rails/pull/264)
+* Fix: "orderable" option has no effect [Issue #245](https://github.com/jbox-web/ajax-datatables-rails/issues/245)
+* Fix: Fix undefined method #and [PR #235](https://github.com/jbox-web/ajax-datatables-rails/pull/235)
+* Add: Add "order nulls last" option [PR #79](https://github.com/jbox-web/ajax-datatables-rails/pull/279)
+* Change: Rename `additional_datas` method as `additional_data` [PR #251](https://github.com/jbox-web/ajax-datatables-rails/pull/251)
+* Change: Added timezone support for daterange [PR #261](https://github.com/jbox-web/ajax-datatables-rails/pull/261)
+* Change: Add # frozen_string_literal: true pragma
+* Various improvements in internal API
+
+## 0.4.0 (2017-05-21)
**Warning:** this version is a **major break** from v0.3. The core has been rewriten to remove dependency on Kaminari (or WillPaginate).
It also brings a new (more natural) way of defining columns, based on hash definitions (and not arrays) and add some filtering options for column search. Take a look at the [README](https://github.com/jbox-web/ajax-datatables-rails#customize-the-generated-datatables-class) for more infos.
-## 0.3.1
+## 0.3.1 (2015-07-13)
* Adds `:oracle` as supported `db_adapter`. Thanks to [lutechspa](https://github.com/lutechspa) for this contribution.
-## 0.3.0
+## 0.3.0 (2015-01-30)
* Changes to the `sortable_columns` and `searchable_columns` syntax as it
required us to do unnecessary guessing. New syntax is `ModelName.column_name`
or `Namespace::ModelName.column_name`. Old syntax of `table_name.column_name`
@@ -20,7 +134,7 @@ It also brings a new (more natural) way of defining columns, based on hash defin
for this contribution.
* Moves paginator settings to configuration initializer.
-## 0.2.1
+## 0.2.1 (2014-11-26)
* Fix count method to work with select statements under Rails 4.1. Thanks to
[Jason Mitchell](https://github.com/mitchej123) for the contribution.
* Edits to `README` documentation about the `options` hash. Thanks to
@@ -38,22 +152,22 @@ text-based columns and perform searches depending on the use of `:mysql2`,
`:sqlite3` or `:pg`. Thanks to [M. Saiqul Haq](https://github.com/saiqulhaq)
for contributing this feature.
-## 0.2.0
+## 0.2.0 (2014-06-19)
* This version works with jQuery dataTables ver. 1.10 and it's new API syntax.
* Added `legacy` branch to repo. If your project is working with jQuery
dataTables ver. 1.9, this is the branch you need to pull, or use the last
`0.1.x` version of this gem.
-## 0.1.2
+## 0.1.2 (2014-06-18)
* Fixes `where` clause being built even when search term is an empty string.
Thanks to [e-fisher](https://github.com/e-fisher) for spotting and fixing this.
-## 0.1.1
+## 0.1.1 (2014-06-13)
* Fixes problem on `searchable_columns` where the corresponding model is
a composite model name, e.g. `UserData`, `BillingAddress`.
Thanks to [iruca3](https://github.com/iruca3) for the fix.
-## 0.1.0
+## 0.1.0 (2014-05-21)
* A fresh start. Sets base class name to: `AjaxDatatablesRails::Base`.
* Extracts pagination functions to mixable modules.
* A user would have the option to stick to the base
@@ -71,3 +185,7 @@ Thanks to [iruca3](https://github.com/iruca3) for the fix.
* Sets generator inside the `Rails` namespace. To generate an
`AjaxDatatablesRails` child class, just execute the
generator like this: `$ rails generate datatable NAME`.
+
+## 0.0.1 (2012-09-10)
+
+First release!
diff --git a/Gemfile b/Gemfile
index 7ae18d32..cc821440 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,8 +1,29 @@
+# frozen_string_literal: true
+
source 'https://rubygems.org/'
gemspec
-# CodeClimate Test Coverage
-group :test do
- gem 'codeclimate-test-reporter', '~> 1.0.0'
-end
+# Dev libs
+gem 'appraisal', git: 'https://github.com/thoughtbot/appraisal.git'
+gem 'combustion'
+gem 'database_cleaner'
+gem 'factory_bot'
+gem 'faker'
+gem 'generator_spec'
+gem 'puma'
+gem 'rake'
+gem 'rspec'
+gem 'rspec-rebound'
+gem 'simplecov'
+
+# Fallback to pg in dev/local environment
+gem 'pg'
+
+# Dev tools / linter
+gem 'guard-rspec', require: false
+gem 'rubocop', require: false
+gem 'rubocop-factory_bot', require: false
+gem 'rubocop-performance', require: false
+gem 'rubocop-rake', require: false
+gem 'rubocop-rspec', require: false
diff --git a/Guardfile b/Guardfile
new file mode 100644
index 00000000..5a44087b
--- /dev/null
+++ b/Guardfile
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+guard :rspec, cmd: 'bin/rspec' do
+ require 'guard/rspec/dsl'
+ dsl = Guard::RSpec::Dsl.new(self)
+
+ # RSpec files
+ rspec = dsl.rspec
+ watch(rspec.spec_helper) { rspec.spec_dir }
+ watch(rspec.spec_support) { rspec.spec_dir }
+ watch(rspec.spec_files)
+
+ # Ruby files
+ ruby = dsl.ruby
+ dsl.watch_spec_files_for(ruby.lib_files)
+end
diff --git a/README.md b/README.md
index 08068fcf..62fd2b29 100644
--- a/README.md
+++ b/README.md
@@ -3,58 +3,42 @@
[](https://github.com/jbox-web/ajax-datatables-rails/blob/master/LICENSE)
[](https://rubygems.org/gems/ajax-datatables-rails)
[](https://rubygems.org/gems/ajax-datatables-rails)
-[](https://travis-ci.org/jbox-web/ajax-datatables-rails)
+[](https://github.com/jbox-web/ajax-datatables-rails/actions)
[](https://codeclimate.com/github/jbox-web/ajax-datatables-rails)
[](https://codeclimate.com/github/jbox-web/ajax-datatables-rails/coverage)
-[](https://gemnasium.com/jbox-web/ajax-datatables-rails)
-> __Important__
->
-> This gem is targeted at Datatables version 1.10.x.
->
-> It's tested against :
-> * Rails 4.0.13 / 4.1.15 / 4.2.9 / 5.0.4 / 5.1.2
-> * Ruby 2.2.7 / 2.3.4 / 2.4.1
-> * Postgresql
-> * MySQL
-> * Oracle XE 11.2 (thanks to [travis-oracle](https://github.com/cbandy/travis-oracle))
-
-## Description
-
-Datatables is a nifty jquery plugin that adds the ability to paginate, sort,
-and search your html tables. When dealing with large tables
-(more than a couple hundred rows) however, we run into performance issues.
-These can be fixed by using server-side pagination, but this breaks some
-datatables functionality.
+**Important : This gem is targeted at DataTables version 1.10.x.**
-`ajax-datatables-rails` is a wrapper around datatable's ajax methods that allow
-synchronization with server-side pagination in a rails app. It was inspired by
-this [Railscast](http://railscasts.com/episodes/340-datatables). I needed to
-implement a similar solution in a couple projects I was working on, so I
-extracted a solution into a gem.
+It's tested against :
-## ORM support
+* Rails: 7.1 / 7.2 / 8.0
+* Ruby: 3.1 / 3.2 / 3.3 / 3.4
+* Databases: MySQL 8 / SQLite3 / Postgresql 16 / Oracle XE 11.2 (thanks to [travis-oracle](https://github.com/cbandy/travis-oracle))
+* Adapters: sqlite / mysql2 / trilogy / postgres / postgis / oracle
-Currently `AjaxDatatablesRails` only supports `ActiveRecord` as ORM for
-performing database queries.
-
-Adding support for `Sequel`, `Mongoid` and `MongoMapper` is a planned feature
-for this gem. If you'd be interested in contributing to speed development,
-please [open an issue](https://github.com/antillas21/ajax-datatables-rails/issues/new)
-and get in touch.
-
-
-## Breaking changes
+## Description
-**Warning:** the v0.4 version is a **major break** from v0.3. The core has been rewriten to remove dependency on Kaminari (or WillPaginate).
+> [DataTables](https://datatables.net/) is a nifty jQuery plugin that adds the ability to paginate, sort, and search your html tables.
+> When dealing with large tables (more than a couple of hundred rows) however, we run into performance issues.
+> These can be fixed by using server-side pagination, but this breaks some DataTables functionality.
+>
+> `ajax-datatables-rails` is a wrapper around DataTables ajax methods that allow synchronization with server-side pagination in a Rails app.
+> It was inspired by this [Railscast](http://railscasts.com/episodes/340-datatables).
+> I needed to implement a similar solution in a couple projects I was working on, so I extracted a solution into a gem.
+>
+> Joel Quenneville (original author)
+>
+> I needed a good gem to manage a lot of DataTables so I chose this one :)
+>
+> Nicolas Rodriguez (current maintainer)
-It also brings a new (more natural) way of defining columns, based on hash definitions (and not arrays) and add some filtering options for column search. [See below](#customize-the-generated-datatables-class) for more infos.
+The final goal of this gem is to **generate a JSON** content that will be given to jQuery DataTables.
+All the datatable customizations (header, tr, td, css classes, width, height, buttons, etc...) **must** take place in the [javascript definition](#5-wire-up-the-javascript) of the datatable.
+jQuery DataTables is a very powerful tool with a lot of customizations available. Take the time to [read the doc](https://datatables.net/reference/option/).
-To migrate on the v0.4 you'll need to :
+You'll find a sample project here : https://ajax-datatables-rails.herokuapp.com
-* update your DataTables classes to remove all the `extend` directives
-* switch to hash definitions of `view_columns`
-* update your views to declare your columns bindings ([See here](#wire-up-the-javascript))
+Its real world examples. The code is here : https://github.com/jbox-web/ajax-datatables-rails-sample-project
## Installation
@@ -62,33 +46,57 @@ To migrate on the v0.4 you'll need to :
Add these lines to your application's Gemfile:
```ruby
-gem 'jquery-datatables-rails'
gem 'ajax-datatables-rails'
```
And then execute:
```sh
-$ bundle
+$ bundle install
```
-The `jquery-datatables-rails` gem is listed as a convenience, to ease adding
-jQuery dataTables to your Rails project. You can always add the plugin assets
-manually via the assets pipeline. If you decide to use the
-`jquery-datatables-rails` gem, please refer to its installation instructions
-[here](https://github.com/rweng/jquery-datatables-rails).
+We assume here that you have already installed [jQuery DataTables](https://datatables.net/).
+
+You can install jQuery DataTables :
+
+* with the [`jquery-datatables`](https://github.com/mkhairi/jquery-datatables) gem
+* by adding the assets manually (in `vendor/assets`)
+* with [Rails webpacker gem](https://github.com/rails/webpacker) (see [here](/doc/webpack.md) for more infos)
-## Usage
+## Note
-*The following examples assume that we are setting up ajax-datatables-rails for
-an index page of users from a `User` model, and that we are using postgresql as
-our db, because you __should be using it__, if not, please refer to the
-[Searching on non text-based columns](#searching-on-non-text-based-columns)
-entry in the Additional Notes section.*
+Currently `AjaxDatatablesRails` only supports `ActiveRecord` as ORM for performing database queries.
+Adding support for `Sequel`, `Mongoid` and `MongoMapper` is (more or less) a planned feature for this gem.
-### Generate
+If you'd be interested in contributing to speed development, please [open an issue](https://github.com/antillas21/ajax-datatables-rails/issues/new) and get in touch.
+
+
+## Quick start (in 5 steps)
+
+The following examples assume that we are setting up `ajax-datatables-rails` for an index page of users from a `User` model,
+and that we are using Postgresql as our db, because you **should be using it**. (It also works with other DB, [see above](#change-the-db-adapter-for-a-datatable-class))
+
+The goal is to render a users table and display : `id`, `first name`, `last name`, `email`, and `bio` for each user.
+
+Something like this:
+
+|ID |First Name|Last Name|Email |Brief Bio|
+|---|----------|---------|----------------------|---------|
+| 1 |John |Doe |john.doe@example.net |Is your default user everywhere|
+| 2 |Jane |Doe |jane.doe@example.net |Is John's wife|
+| 3 |James |Doe |james.doe@example.net |Is John's brother and best friend|
+
+Here the steps we're going through :
+
+1. [Generate the datatable class](#1-generate-the-datatable-class)
+2. [Build the View](#2-build-the-view)
+3. [Customize the generated Datatables class](#3-customize-the-generated-datatables-class)
+4. [Setup the Controller action](#4-setup-the-controller-action)
+5. [Wire up the Javascript](#5-wire-up-the-javascript)
+
+### 1) Generate the datatable class
Run the following command:
@@ -102,31 +110,24 @@ Open the file and customize in the functions as directed by the comments.
Take a look [here](#generator-syntax) for an explanation about the generator syntax.
-### Build the View
-
-You should always start by the single source of truth, which is your html view. Suppose we need to render a users table and display: first name, last name, and bio for each user.
-
-Something like this:
-
-|First Name|Last Name|Brief Bio|
-|----------|---------|---------|
-|John |Doe |Is your default user everywhere|
-|Jane |Doe |Is John's wife|
-|James |Doe |Is John's brother and best friend|
+### 2) Build the View
+You should always start by the single source of truth, which is your html view.
* Set up an html `
` with a `` and ``
* Add in your table headers if desired
-* Don't add any rows to the body of the table, datatables does this automatically
+* Don't add any rows to the body of the table, DataTables does this automatically
* Add a data attribute to the `` tag with the url of the JSON feed, in our case is the `users_path` as we're pointing to the `UsersController#index` action
```html
-
+
+ | ID |
First Name |
Last Name |
+ Email |
Brief Bio |
@@ -136,25 +137,22 @@ Something like this:
```
-### Customize the generated Datatables class
+### 3) Customize the generated Datatables class
-```ruby
-def view_columns
- # Declare strings in this format: ModelName.column_name
- # or in aliased_join_table.column_name format
- @view_columns ||= {}
-end
-```
+#### a. Declare columns mapping
-* In this method, add a list of the model(s) columns mapped to the data you need to present. In this case: `first_name`, `last_name` and `bio`.
+First we need to declare in `view_columns` the list of the model(s) columns mapped to the data we need to present.
+In this case: `id`, `first_name`, `last_name`, `email` and `bio`.
This gives us:
```ruby
def view_columns
@view_columns ||= {
+ id: { source: "User.id" },
first_name: { source: "User.first_name", cond: :like, searchable: true, orderable: true },
- last_name: { source: "User.last_name", cond: :like },
+ last_name: { source: "User.last_name", cond: :like, nulls_last: true },
+ email: { source: "User.email" },
bio: { source: "User.bio" },
}
end
@@ -164,53 +162,60 @@ end
`cond` can be :
-* `:like`, `:start_with`, `:end_with` for string or full text search
+* `:like`, `:start_with`, `:end_with`, `:string_eq`, `:string_in` for string or full text search
* `:eq`, `:not_eq`, `:lt`, `:gt`, `:lteq`, `:gteq`, `:in` for numeric
-* `:date_range` for date range (only for Rails > 4.2.x)
+* `:date_range` for date range
* `:null_value` for nil field
-* `Proc` for whatever (see [here](https://github.com/ajahongir/ajax-datatables-rails-v-0-4-0-how-to/blob/master/app/datatables/city_datatable.rb) for real example)
+* `Proc` for whatever (see [here](https://github.com/jbox-web/ajax-datatables-rails-sample-project/blob/master/app/datatables/city_datatable.rb) for real example)
-[See here](#searching-on-non-text-based-columns) for notes about the `view_columns` settings (if using something different from `postgres`).
-[Read these notes](#columns-syntax) about considerations for the `view_columns` method.
+The `nulls_last` param allows for nulls to be ordered last. You can configure it by column, like above, or by datatable class :
+```ruby
+class MyDatatable < AjaxDatatablesRails::ActiveRecord
+ self.nulls_last = true
-#### Map data
+ # ... other methods (view_columns, data...)
+end
+```
+
+See [here](#columns-syntax) to get more details about columns definitions and how to play with associated models.
+
+You can customize or sanitize the search value passed to the DB by using the `:formatter` option with a lambda :
```ruby
-def data
- records.map do |record|
- {
- # a hash of key value pairs
- }
- end
+def view_columns
+ @view_columns ||= {
+ id: { source: "User.id" },
+ first_name: { source: "User.first_name" },
+ last_name: { source: "User.last_name" },
+ email: { source: "User.email", formatter: -> (o) { o.upcase } },
+ bio: { source: "User.bio" },
+ }
end
```
+The object passed to the lambda is the search value.
+
+#### b. Map data
+
+Then we need to map the records retrieved by the `get_raw_records` method to the real values we want to display :
+
```ruby
def data
records.map do |record|
{
+ id: record.id,
first_name: record.first_name,
last_name: record.last_name,
+ email: record.email,
bio: record.bio,
- # 'DT_RowId' => record.id, # This will set the id attribute on the corresponding in the datatable
+ DT_RowId: record.id, # This will automagically set the id attribute on the corresponding
in the datatable
}
end
end
```
-You can either use the v0.3 Array style for your columns :
-
-```ruby
-def data
- records.map do |record|
- [
- # comma separated list of the values for each cell of a table row
- # example: record.first_name, record.last_name
- ]
- end
-end
-```
+**Deprecated:** You can either use the v0.3 Array style for your columns :
This method builds a 2d array that is used by datatables to construct the html
table. Insert the values you want on each column.
@@ -219,34 +224,27 @@ table. Insert the values you want on each column.
def data
records.map do |record|
[
+ record.id,
record.first_name,
record.last_name,
+ record.email,
record.bio
]
end
end
```
-[See here](#using-view-helpers) if you need to use view helpers like `link_to`, `mail_to`, `resource_path`, etc.
-
+The drawback of this method is that you can't pass the `DT_RowId` so it's tricky to set the id attribute on the corresponding `
` in the datatable (need to be done on JS side).
-#### Get Raw Records
+[See here](#using-view-helpers) if you need to use view helpers like `link_to`, `mail_to`, etc...
-```ruby
-def get_raw_records
- # insert query here
-end
-```
+#### c. Get Raw Records
This is where your query goes.
```ruby
def get_raw_records
- # suppose we need all User records
- # Rails 4+
User.all
- # Rails 3.x
- # User.scoped
end
```
@@ -260,115 +258,27 @@ def get_raw_records
end
```
-You can put any logic in `get_raw_records` [based on any parameters you inject](#options) in the `Datatable` object.
-
-> __IMPORTANT:__ Make sure to return an `ActiveRecord::Relation` object
-> as the end product of this method.
->
-> Why? Because the result from this method, will be chained (for now)
-> to `ActiveRecord` methods for sorting, filtering and pagination.
+You can put any logic in `get_raw_records` [based on any parameters you inject](#pass-options-to-the-datatable-class) in the `Datatable` object.
+**IMPORTANT :** Because the result of this method will be chained to `ActiveRecord` methods for sorting, filtering and pagination,
+make sure to return an `ActiveRecord::Relation` object.
-#### Associated and nested models
-
-The previous example has only one single model. But what about if you have
-some associated nested models and in a report you want to show fields from
-these tables.
-
-Take an example that has an `Event, Course, Coursetype, Allocation, Teacher,
-Contact, Competency and CompetencyType` models. We want to have a datatables
-report which has the following column:
-
-```ruby
-'coursetypes.name',
-'courses.name',
-'events.title',
-'events.event_start',
-'events.event_end',
-'contacts.full_name',
-'competency_types.name',
-'events.status'
-```
+#### d. Additional data
-We want to sort and search on all columns of the list. The related definition
-would be:
+You can inject other key/value pairs in the rendered JSON by defining the `#additional_data` method :
```ruby
-def view_columns
- @view_columns ||= [
- 'Coursetype.name',
- 'Course.name',
- 'Event.title',
- 'Event.event_start',
- 'Event.event_end',
- 'Contact.last_name',
- 'CompetencyType.name',
- 'Event.status'
- ]
-end
-
-def get_raw_records
- Event.joins(
- { course: :coursetype },
- { allocations: {
- teacher: [:contact, {competencies: :competency_type}]
- }
- }).distinct
-end
-```
-
-__Some comments for the above code:__
-
-1. In the `get_raw_records` method we have quite a complex query having one to
-many and may to many associations using the joins ActiveRecord method.
-The joins will generate INNER JOIN relations in the SQL query. In this case,
-we do not include all event in the report if we have events which is not
-associated with any model record from the relation.
-
-2. To have all event records in the list we should use the `.includes` method,
-which generate LEFT OUTER JOIN relation of the SQL query.
-__IMPORTANT:__ Make sure to append `.references(:related_model)` with any
-associated model. That forces the eager loading of all the associated models
-by one SQL query, and the search condition for any column works fine.
-Otherwise the `:recordsFiltered => filter_records(get_raw_records).count(:all)`
-will generate 2 SQL queries (one for the Event model, and then another for the
-associated tables). The `:recordsFiltered => filter_records(get_raw_records).count(:all)`
-will use only the first one to return from the ActiveRecord::Relation object
-in `get_raw_records` and you will get an error message of __Unknown column
-'yourtable.yourfield' in 'where clause'__ in case the search field value
-is not empty.
-
-So the query using the `.includes()` method is:
-
-```ruby
-def get_raw_records
- Event.includes(
- { course: :coursetype },
- { allocations: {
- teacher: [:contact, { competencies: :competency_type }]
- }
- }
- ).references(:course).distinct
-end
-```
-
-
-#### Additional datas
-
-You can inject other key/value pairs in the rendered JSON by defining the `#additional_datas` method :
-
-```ruby
-def additional_datas
+def additional_data
{
foo: 'bar'
}
end
```
-Very useful with https://github.com/vedmack/yadcf to provide values for dropdown filters.
+Very useful with [datatables-factory](https://github.com/jbox-web/datatables-factory) (or [yadcf](https://github.com/vedmack/yadcf)) to provide values for dropdown filters.
-### Setup the Controller action
+### 4) Setup the Controller action
Set the controller to respond to JSON
@@ -376,16 +286,18 @@ Set the controller to respond to JSON
def index
respond_to do |format|
format.html
- format.json { render json: UserDatatable.new(view_context) }
+ format.json { render json: UserDatatable.new(params) }
end
end
```
Don't forget to make sure the proper route has been added to `config/routes.rb`.
-[See here](#options) to inject params in the `UserDatatable`.
+[See here](#pass-options-to-the-datatable-class) if you need to inject params in the `UserDatatable`.
+
+**Note :** If you have more than **2** datatables in your application, don't forget to read [this](#use-http-post-method-medium).
-### Wire up the Javascript
+### 5) Wire up the Javascript
Finally, the javascript to tie this all together. In the appropriate `coffee` file:
@@ -393,14 +305,17 @@ Finally, the javascript to tie this all together. In the appropriate `coffee` fi
# users.coffee
$ ->
- $('#users-table').dataTable
+ $('#users-datatable').dataTable
processing: true
serverSide: true
- ajax: $('#users-table').data('source')
+ ajax:
+ url: $('#users-datatable').data('source')
pagingType: 'full_numbers'
columns: [
+ {data: 'id'}
{data: 'first_name'}
{data: 'last_name'}
+ {data: 'email'}
{data: 'bio'}
]
# pagingType is optional, if you want full pagination controls.
@@ -409,18 +324,23 @@ $ ->
```
or, if you're using plain javascript:
+
```javascript
// users.js
jQuery(document).ready(function() {
- $('#users-table').dataTable({
+ $('#users-datatable').dataTable({
"processing": true,
"serverSide": true,
- "ajax": $('#users-table').data('source'),
+ "ajax": {
+ "url": $('#users-datatable').data('source')
+ },
"pagingType": "full_numbers",
"columns": [
+ {"data": "id"},
{"data": "first_name"},
{"data": "last_name"},
+ {"data": "email"},
{"data": "bio"}
]
// pagingType is optional, if you want full pagination controls.
@@ -430,192 +350,300 @@ jQuery(document).ready(function() {
});
```
+## Advanced usage
-### Additional Notes
+### Using view helpers
-#### Columns syntax
+Sometimes you'll need to use view helper methods like `link_to`, `mail_to`,
+`edit_user_path`, `check_box_tag` and so on in the returned JSON representation returned by the [`data`](#b-map-data) method.
-Since version `0.3.0`, we are implementing a pseudo code way of declaring
-the array columns to use when querying the database.
-
-Example. Suppose we have the following models: `User`, `PurchaseOrder`,
-`Purchase::LineItem` and we need to have several columns from those models
-available in our datatable to search and sort by.
+To have these methods available to be used, this is the way to go:
```ruby
-# we use the ModelName.column_name notation to declare our columns
+class UserDatatable < AjaxDatatablesRails::ActiveRecord
+ extend Forwardable
-def view_columns
- @view_columns ||= [
- 'User.first_name',
- 'User.last_name',
- 'PurchaseOrder.number',
- 'PurchaseOrder.created_at',
- 'Purchase::LineItem.quantity',
- 'Purchase::LineItem.unit_price',
- 'Purchase::LineItem.item_total'
- ]
-end
-```
+ # either define them one-by-one
+ def_delegator :@view, :check_box_tag
+ def_delegator :@view, :link_to
+ def_delegator :@view, :mail_to
+ def_delegator :@view, :edit_user_path
+ # or define them in one pass
+ def_delegators :@view, :check_box_tag, :link_to, :mail_to, :edit_user_path
-#### What if the datatable itself is namespaced?
+ # ... other methods (view_columns, get_raw_records...)
-Example: what if the datatable is namespaced into an `Admin` module?
+ def initialize(params, opts = {})
+ @view = opts[:view_context]
+ super
+ end
-```ruby
-module Admin
- class PurchasesDatatable < AjaxDatatablesRails::Base
+ # now, you'll have these methods available to be used anywhere
+ def data
+ records.map do |record|
+ {
+ id: check_box_tag('users[]', record.id),
+ first_name: link_to(record.first_name, edit_user_path(record)),
+ last_name: record.last_name,
+ email: mail_to(record.email),
+ bio: record.bio
+ DT_RowId: record.id,
+ }
+ end
end
end
-```
-
-Taking the same models and columns, we would define it like this:
-```ruby
-def view_columns
- @view_columns ||= [
- '::User.first_name',
- '::User.last_name',
- '::PurchaseOrder.number',
- '::PurchaseOrder.created_at',
- '::Purchase::LineItem.quantity',
- '::Purchase::LineItem.unit_price',
- '::Purchase::LineItem.item_total'
- ]
+# and in your controller:
+def index
+ respond_to do |format|
+ format.html
+ format.json { render json: UserDatatable.new(params, view_context: view_context) }
+ end
end
```
-Pretty much like you would do it, if you were inside a namespaced controller.
+### Using view decorators
+If you want to keep things tidy in the data mapping method, you could use
+[Draper](https://github.com/drapergem/draper) to define column mappings like below.
-#### Searching on non text-based columns
+**Note :** This is the recommanded way as you don't need to inject the `view_context` in the Datatable object to access helpers methods.
+It also helps in separating view/presentation logic from filtering logic (the only one that really matters in a datatable class).
-It always comes the time when you need to add a non-string/non-text based
-column to the `@view_columns` array, so you can perform searches against
-these column types (example: numeric, date, time).
+Example :
-We recently added the ability to (automatically) typecast these column types
-and have this scenario covered. Please note however, if you are using
-something different from `postgresql` (with the `:pg` gem), like `mysql` or
-`sqlite`, then you need to add an initializer in your application's
-`config/initializers` directory.
+```ruby
+class UserDatatable < AjaxDatatablesRails::ActiveRecord
+ ...
+ def data
+ records.map do |record|
+ {
+ id: record.decorate.check_box,
+ first_name: record.decorate.link_to,
+ last_name: record.decorate.last_name
+ email: record.decorate.email,
+ bio: record.decorate.bio
+ DT_RowId: record.id,
+ }
+ end
+ end
+ ...
+end
-If you don't perform this step (again, if using something different from
-`postgresql`), your database will complain that it does not understand the
-default typecast used to enable such searches.
+class UserDecorator < ApplicationDecorator
+ delegate :last_name, :bio
+ def check_box
+ h.check_box_tag 'users[]', object.id
+ end
-#### Configuration initializer
+ def link_to
+ h.link_to object.first_name, h.edit_user_path(object)
+ end
-You have two options to create this initializer:
+ def email
+ h.mail_to object.email
+ end
-* use the provided (and recommended) generator (and then just edit the file);
-* create the file from scratch.
+ # Just an example of a complex method you can add to you decorator
+ # To render it in a datatable just add a column 'dt_actions' in
+ # 'view_columns' and 'data' methods and call record.decorate.dt_actions
+ def dt_actions
+ links = []
+ links << h.link_to 'Edit', h.edit_user_path(object) if h.policy(object).update?
+ links << h.link_to 'Delete', h.user_path(object), method: :delete, remote: true if h.policy(object).destroy?
+ h.safe_join(links, '')
+ end
+end
+```
-To use the generator, from the terminal execute:
+### Pass options to the datatable class
-```sh
-$ bundle exec rails generate datatable:config
-```
+An `AjaxDatatablesRails::ActiveRecord` inherited class can accept an options hash at initialization. This provides room for flexibility when required.
-Doing so, will create the `config/initializers/ajax_datatables_rails.rb` file
-with the following content:
+Example:
```ruby
-AjaxDatatablesRails.configure do |config|
- # available options for db_adapter are: :pg, :mysql, :mysql2, :sqlite, :sqlite3
- # config.db_adapter = :pg
-
- # available options for orm are: :active_record, :mongoid
- # config.orm = :active_record
+# In the controller
+def index
+ respond_to do |format|
+ format.html
+ format.json { render json: UserDatatable.new(params, user: current_user, from: 1.month.ago) }
+ end
end
-```
-Uncomment the `config.db_adapter` line and set the corresponding value to your
-database and gem. This is all you need.
+# The datatable class
+class UnrespondedMessagesDatatable < AjaxDatatablesRails::ActiveRecord
-Uncomment the `config.orm` line to set `active_record or mongoid` if
-included in your project. It defaults to `active_record`.
+ # ... other methods (view_columns, data...)
-If you want to make the file from scratch, just copy the above code block into
-a file inside the `config/initializers` directory.
+ def user
+ @user ||= options[:user]
+ end
+ def from
+ @from ||= options[:from].beginning_of_day
+ end
-#### Using view helpers
+ def to
+ @to ||= Date.today.end_of_day
+ end
-Sometimes you'll need to use view helper methods like `link_to`, `h`, `mailto`,
-`edit_resource_path` in the returned JSON representation returned by the `data`
-method.
+ # We can now customize the get_raw_records method
+ # with the options we've injected
+ def get_raw_records
+ user.messages.unresponded.where(received_at: from..to)
+ end
-To have these methods available to be used, this is the way to go:
+end
+```
+
+### Change the DB adapter for a datatable class
+
+If you have models from different databases you can set the `db_adapter` on the datatable class :
```ruby
-class MyCustomDatatable < AjaxDatatablesRails::Base
- # either define them one-by-one
- def_delegator :@view, :link_to
- def_delegator :@view, :h
- def_delegator :@view, :mail_to
+class MySharedModelDatatable < AjaxDatatablesRails::ActiveRecord
+ self.db_adapter = :oracle_enhanced
- # or define them in one pass
- def_delegators :@view, :link_to, :h, :mailto, :edit_resource_path, :other_method
+ # ... other methods (view_columns, data...)
- # now, you'll have these methods available to be used anywhere
- # example: mapping the 2d jsonified array returned.
- def data
- records.map do |record|
- {
- first_name: link_to(record.fname, edit_resource_path(record)),
- email: mail_to(record.email),
- # other attributes
- }
+ def get_raw_records
+ AnimalsRecord.connected_to(role: :reading) do
+ Dog.all
end
end
end
```
+### Columns syntax
-#### Options
+You can mix several model in the same datatable.
-An `AjaxDatatablesRails::Base` inherited class can accept an options hash at
-initialization. This provides room for flexibility when required. Example:
+Suppose we have the following models: `User`, `PurchaseOrder`,
+`Purchase::LineItem` and we need to have several columns from those models
+available in our datatable to search and sort by.
```ruby
-class UnrespondedMessagesDatatable < AjaxDatatablesRails::Base
- # customized methods here
-end
+# we use the ModelName.column_name notation to declare our columns
-datatable = UnrespondedMessagesDatatable.new(view_context,
- { user: current_user, from: 1.month.ago }
-)
+def view_columns
+ @view_columns ||= {
+ first_name: { source: 'User.first_name' },
+ last_name: { source: 'User.last_name' },
+ order_number: { source: 'PurchaseOrder.number' },
+ order_created_at: { source: 'PurchaseOrder.created_at' },
+ quantity: { source: 'Purchase::LineItem.quantity' },
+ unit_price: { source: 'Purchase::LineItem.unit_price' },
+ item_total: { source: 'Purchase::LineItem.item_total }'
+ }
+end
```
-So, now inside your class code, you can use those options like this:
+### Associated and nested models
+The previous example has only one single model. But what about if you have
+some associated nested models and in a report you want to show fields from
+these tables.
+
+Take an example that has an `Event, Course, CourseType, Allocation, Teacher,
+Contact, Competency and CompetencyType` models. We want to have a datatables
+report which has the following column:
```ruby
-# let's see an example
-def user
- @user ||= options[:user]
-end
+'course_types.name'
+'courses.name'
+'contacts.full_name'
+'competency_types.name'
+'events.title'
+'events.event_start'
+'events.event_end'
+'events.status'
+```
+
+We want to sort and search on all columns of the list.
+The related definition would be :
-def from
- @from ||= options[:from].beginning_of_day
+```ruby
+def view_columns
+ @view_columns ||= {
+ course_type: { source: 'CourseType.name' },
+ course_name: { source: 'Course.name' },
+ contact_name: { source: 'Contact.full_name' },
+ competency_type: { source: 'CompetencyType.name' },
+ event_title: { source: 'Event.title' },
+ event_start: { source: 'Event.event_start' },
+ event_end: { source: 'Event.event_end' },
+ event_status: { source: 'Event.status' },
+ }
end
-def to
- @to ||= Date.today.end_of_day
+def get_raw_records
+ Event.joins(
+ { course: :course_type },
+ { allocations: {
+ teacher: [:contact, { competencies: :competency_type }]
+ }
+ }).distinct
end
+```
+
+**Some comments for the above code :**
+
+1. In the `get_raw_records` method we have quite a complex query having one to
+many and many to many associations using the joins ActiveRecord method.
+The joins will generate INNER JOIN relations in the SQL query. In this case,
+we do not include all event in the report if we have events which is not
+associated with any model record from the relation.
+2. To have all event records in the list we should use the `.includes` method,
+which generate LEFT OUTER JOIN relation of the SQL query.
+
+**IMPORTANT :**
+
+Make sure to append `.references(:related_model)` with any
+associated model. That forces the eager loading of all the associated models
+by one SQL query, and the search condition for any column works fine.
+Otherwise the `:recordsFiltered => filter_records(get_raw_records).count(:all)`
+will generate 2 SQL queries (one for the Event model, and then another for the
+associated tables). The `:recordsFiltered => filter_records(get_raw_records).count(:all)`
+will use only the first one to return from the ActiveRecord::Relation object
+in `get_raw_records` and you will get an error message of **Unknown column
+'yourtable.yourfield' in 'where clause'** in case the search field value
+is not empty.
+
+So the query using the `.includes()` method is:
+
+```ruby
def get_raw_records
- user.messages.unresponded.where(received_at: from..to)
+ Event.includes(
+ { course: :course_type },
+ { allocations: {
+ teacher: [:contact, { competencies: :competency_type }]
+ }
+ }).references(:course).distinct
end
```
+### Default scope
+
+See [DefaultScope is evil](https://rails-bestpractices.com/posts/2013/06/15/default_scope-is-evil/) and [#223](https://github.com/jbox-web/ajax-datatables-rails/issues/223) and [#233](https://github.com/jbox-web/ajax-datatables-rails/issues/233).
+
+### DateRange search
-#### Generator Syntax
+This feature works with [datatables-factory](https://github.com/jbox-web/datatables-factory) (or [yadcf](https://github.com/vedmack/yadcf)).
-Also, a class that inherits from `AjaxDatatablesRails::Base` is not tied to an
+To enable the date range search, for example `created_at` :
+
+* add a `created_at` `` in your html
+* declare your column in `view_columns` : `created_at: { source: 'Post.created_at', cond: :date_range, delimiter: '-yadcf_delim-' }`
+* add it in `data` : `created_at: record.decorate.created_at`
+* setup yadcf to make `created_at` search field a range
+
+### Generator Syntax
+
+Also, a class that inherits from `AjaxDatatablesRails::ActiveRecord` is not tied to an
existing model, module, constant or any type of class in your Rails app.
You can pass a name to your datatable class like this:
@@ -635,19 +663,156 @@ In the end, it's up to the developer which model(s), scope(s), relationship(s)
(or else) to employ inside the datatable class to retrieve records from the
database.
+## Tests
-## Tutorial
+Datatables can be tested with Capybara provided you don't use Webrick during integration tests.
+
+Long story short and as a rule of thumb : use the same webserver everywhere (dev, prod, staging, test, etc...).
+
+If you use Puma (the Rails default webserver), use Puma everywhere, even in CI/test environment. The same goes for Thin.
+
+You will avoid the usual story : it works in dev but not in test environment...
+
+If you want to test datatables with a lot of data you might need this kind of tricks : https://robots.thoughtbot.com/automatically-wait-for-ajax-with-capybara. (thanks CharlieIGG)
+
+## ProTipsâ„¢
+
+### Create a master parent class (Easy)
+
+In the same spirit of Rails `ApplicationController` and `ApplicationRecord`, you can create an `ApplicationDatatable` class (in `app/datatables/application_datatable.rb`)
+that will be inherited from other classes :
+
+```ruby
+class ApplicationDatatable < AjaxDatatablesRails::ActiveRecord
+ # puts commonly used methods here
+end
+
+class PostDatatable < ApplicationDatatable
+end
+```
+
+This way it will be easier to DRY you datatables.
+
+### Speedup JSON rendering (Easy)
+
+Install [yajl-ruby](https://github.com/brianmario/yajl-ruby), basically :
+
+```ruby
+gem 'yajl-ruby', require: 'yajl'
+```
+
+then
+
+```sh
+$ bundle install
+```
+
+That's all :) ([Automatically prefer Yajl or JSON backend over Yaml, if available](https://github.com/rails/rails/commit/63bb955a99eb46e257655c93dd64e86ebbf05651))
-Tutorial for Integrating `ajax-datatables-rails` on Rails 4.
+### Use HTTP `POST` method (Medium)
-[Part-1 The-Installation](https://github.com/jbox-web/ajax-datatables-rails/wiki/Part-1----The-Installation)
+Use HTTP `POST` method to avoid `414 Request-URI Too Large` error. See : [#278](https://github.com/jbox-web/ajax-datatables-rails/issues/278) and [#308](https://github.com/jbox-web/ajax-datatables-rails/issues/308#issuecomment-424897335).
-[Part 2 The Datatables with ajax functionality](https://github.com/jbox-web/ajax-datatables-rails/wiki/Part-2-The-Datatables-with-ajax-functionality)
+You can easily define a route concern in `config/routes.rb` and reuse it when you need it :
+
+```ruby
+Rails.application.routes.draw do
+ concern :with_datatable do
+ post 'datatable', on: :collection
+ end
+
+ resources :posts, concerns: [:with_datatable]
+ resources :users, concerns: [:with_datatable]
+end
+```
+
+then in your controllers :
+
+```ruby
+# PostsController
+ def index
+ end
+
+ def datatable
+ render json: PostDatatable.new(params)
+ end
+
+# UsersController
+ def index
+ end
+
+ def datatable
+ render json: UserDatatable.new(params)
+ end
+```
+
+then in your views :
+
+```html
+# posts/index.html.erb
+
+
+# users/index.html.erb
+
+```
+
+then in your Coffee/JS :
+
+```coffee
+# send params in form data
+$ ->
+ $('#posts-datatable').dataTable
+ ajax:
+ url: $('#posts-datatable').data('source')
+ type: 'POST'
+ # ...others options, see [here](#5-wire-up-the-javascript)
+
+# send params as json data
+$ ->
+ $('#users-datatable').dataTable
+ ajax:
+ url: $('#users-datatable').data('source')
+ contentType: 'application/json'
+ type: 'POST'
+ data: (d) ->
+ JSON.stringify d
+ # ...others options, see [here](#5-wire-up-the-javascript)
+```
+
+### Create indices for Postgresql (Expert)
+
+In order to speed up the `ILIKE` queries that are executed when using the default configuration, you might want to consider adding some indices.
+For postgresql, you are advised to use the [gin/gist index type](http://www.postgresql.org/docs/current/interactive/pgtrgm.html).
+This makes it necessary to enable the postgrsql extension `pg_trgm`. Double check that you have this extension installed before trying to enable it.
+A migration for enabling the extension and creating the indices could look like this:
+
+```ruby
+def change
+ enable_extension :pg_trgm
+ TEXT_SEARCH_ATTRIBUTES = ['your', 'attributes']
+ TABLE = 'your_table'
+
+ TEXT_SEARCH_ATTRIBUTES.each do |attr|
+ reversible do |dir|
+ dir.up do
+ execute "CREATE INDEX #{TABLE}_#{attr}_gin ON #{TABLE} USING gin(#{attr} gin_trgm_ops)"
+ end
+
+ dir.down do
+ remove_index TABLE.to_sym, name: "#{TABLE}_#{attr}_gin"
+ end
+ end
+ end
+end
+```
+
+## Tutorial
-The complete project code for this tutorial series is available on [github](https://github.com/trkrameshkumar/simple_app).
+Filtering by JSONB column values : [#277](https://github.com/jbox-web/ajax-datatables-rails/issues/277#issuecomment-366526373)
-Another sample project [code](https://github.com/ajahongir/ajax-datatables-rails-v-0-4-0-how-to). Its real world example.
+Use [has_scope](https://github.com/plataformatec/has_scope) gem with `ajax-datatables-rails` : [#280](https://github.com/jbox-web/ajax-datatables-rails/issues/280)
+Use [Datatable orthogonal data](https://datatables.net/manual/data/orthogonal-data) : see [#269](https://github.com/jbox-web/ajax-datatables-rails/issues/269#issuecomment-387940478)
## Contributing
diff --git a/Rakefile b/Rakefile
index fa3c4583..ad26ae7d 100644
--- a/Rakefile
+++ b/Rakefile
@@ -1,14 +1,7 @@
-#!/usr/bin/env rake
+# frozen_string_literal: true
+
require 'bundler/gem_tasks'
require 'rspec/core/rake_task'
RSpec::Core::RakeTask.new(:spec)
task default: :spec
-
-task :console do
- require 'pry'
- require 'rails'
- require 'ajax-datatables-rails'
- ARGV.clear
- Pry.start
-end
diff --git a/ajax-datatables-rails.gemspec b/ajax-datatables-rails.gemspec
index 36052a61..69e22ea8 100644
--- a/ajax-datatables-rails.gemspec
+++ b/ajax-datatables-rails.gemspec
@@ -1,37 +1,29 @@
-# coding: utf-8
-$:.push File.expand_path('../lib', __FILE__)
-require 'ajax-datatables-rails/version'
+# frozen_string_literal: true
+
+require_relative 'lib/ajax-datatables-rails/version'
Gem::Specification.new do |s|
s.name = 'ajax-datatables-rails'
- s.version = AjaxDatatablesRails::VERSION
+ s.version = AjaxDatatablesRails::VERSION::STRING
s.platform = Gem::Platform::RUBY
s.authors = ['Joel Quenneville', 'Antonio Antillon']
s.email = ['joel.quenneville@collegeplus.org', 'antillas21@gmail.com']
s.homepage = 'https://github.com/jbox-web/ajax-datatables-rails'
- s.summary = %q{A gem that simplifies using datatables and hundreds of records via ajax}
- s.description = %q{A wrapper around datatable's ajax methods that allow synchronization with server-side pagination in a rails app}
+ s.summary = 'A gem that simplifies using datatables and hundreds of records via ajax'
+ s.description = "A wrapper around datatable's ajax methods that allow synchronization with server-side pagination in a rails app"
s.license = 'MIT'
+ s.metadata = {
+ 'homepage_uri' => 'https://github.com/jbox-web/ajax-datatables-rails',
+ 'changelog_uri' => 'https://github.com/jbox-web/ajax-datatables-rails/blob/master/CHANGELOG.md',
+ 'source_code_uri' => 'https://github.com/jbox-web/ajax-datatables-rails',
+ 'bug_tracker_uri' => 'https://github.com/jbox-web/ajax-datatables-rails/issues',
+ 'rubygems_mfa_required' => 'true',
+ }
- s.add_dependency 'railties', '>= 4.0'
+ s.required_ruby_version = '>= 3.1.0'
- s.add_development_dependency 'rails', '>= 4.0'
- s.add_development_dependency 'rake'
- s.add_development_dependency 'pg'
- s.add_development_dependency 'mysql2'
- s.add_development_dependency 'sqlite3'
- s.add_development_dependency 'activerecord-oracle_enhanced-adapter'
- s.add_development_dependency 'rspec'
- s.add_development_dependency 'generator_spec'
- s.add_development_dependency 'pry'
- s.add_development_dependency 'simplecov'
- s.add_development_dependency 'database_cleaner'
- s.add_development_dependency 'factory_girl'
- s.add_development_dependency 'faker'
- s.add_development_dependency 'appraisal'
+ s.files = Dir['README.md', 'CHANGELOG.md', 'LICENSE', 'lib/**/*.rb', 'lib/**/*.erb']
- s.files = `git ls-files`.split("\n")
- s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
- s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
- s.require_paths = ['lib']
+ s.add_dependency 'rails', '>= 7.1'
+ s.add_dependency 'zeitwerk'
end
diff --git a/bin/_guard-core b/bin/_guard-core
new file mode 100755
index 00000000..9105b28b
--- /dev/null
+++ b/bin/_guard-core
@@ -0,0 +1,27 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+#
+# This file was generated by Bundler.
+#
+# The application '_guard-core' is installed as part of a gem, and
+# this file is here to facilitate running it.
+#
+
+ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
+
+bundle_binstub = File.expand_path("bundle", __dir__)
+
+if File.file?(bundle_binstub)
+ if File.read(bundle_binstub, 300).include?("This file was generated by Bundler")
+ load(bundle_binstub)
+ else
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
+Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
+ end
+end
+
+require "rubygems"
+require "bundler/setup"
+
+load Gem.bin_path("guard", "_guard-core")
diff --git a/bin/appraisal b/bin/appraisal
new file mode 100755
index 00000000..5038ce52
--- /dev/null
+++ b/bin/appraisal
@@ -0,0 +1,27 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+#
+# This file was generated by Bundler.
+#
+# The application 'appraisal' is installed as part of a gem, and
+# this file is here to facilitate running it.
+#
+
+ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
+
+bundle_binstub = File.expand_path("bundle", __dir__)
+
+if File.file?(bundle_binstub)
+ if File.read(bundle_binstub, 300).include?("This file was generated by Bundler")
+ load(bundle_binstub)
+ else
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
+Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
+ end
+end
+
+require "rubygems"
+require "bundler/setup"
+
+load Gem.bin_path("appraisal", "appraisal")
diff --git a/bin/bundle b/bin/bundle
new file mode 100755
index 00000000..50da5fdf
--- /dev/null
+++ b/bin/bundle
@@ -0,0 +1,109 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+#
+# This file was generated by Bundler.
+#
+# The application 'bundle' is installed as part of a gem, and
+# this file is here to facilitate running it.
+#
+
+require "rubygems"
+
+m = Module.new do
+ module_function
+
+ def invoked_as_script?
+ File.expand_path($0) == File.expand_path(__FILE__)
+ end
+
+ def env_var_version
+ ENV["BUNDLER_VERSION"]
+ end
+
+ def cli_arg_version
+ return unless invoked_as_script? # don't want to hijack other binstubs
+ return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update`
+ bundler_version = nil
+ update_index = nil
+ ARGV.each_with_index do |a, i|
+ if update_index && update_index.succ == i && a.match?(Gem::Version::ANCHORED_VERSION_PATTERN)
+ bundler_version = a
+ end
+ next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/
+ bundler_version = $1
+ update_index = i
+ end
+ bundler_version
+ end
+
+ def gemfile
+ gemfile = ENV["BUNDLE_GEMFILE"]
+ return gemfile if gemfile && !gemfile.empty?
+
+ File.expand_path("../Gemfile", __dir__)
+ end
+
+ def lockfile
+ lockfile =
+ case File.basename(gemfile)
+ when "gems.rb" then gemfile.sub(/\.rb$/, ".locked")
+ else "#{gemfile}.lock"
+ end
+ File.expand_path(lockfile)
+ end
+
+ def lockfile_version
+ return unless File.file?(lockfile)
+ lockfile_contents = File.read(lockfile)
+ return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/
+ Regexp.last_match(1)
+ end
+
+ def bundler_requirement
+ @bundler_requirement ||=
+ env_var_version ||
+ cli_arg_version ||
+ bundler_requirement_for(lockfile_version)
+ end
+
+ def bundler_requirement_for(version)
+ return "#{Gem::Requirement.default}.a" unless version
+
+ bundler_gem_version = Gem::Version.new(version)
+
+ bundler_gem_version.approximate_recommendation
+ end
+
+ def load_bundler!
+ ENV["BUNDLE_GEMFILE"] ||= gemfile
+
+ activate_bundler
+ end
+
+ def activate_bundler
+ gem_error = activation_error_handling do
+ gem "bundler", bundler_requirement
+ end
+ return if gem_error.nil?
+ require_error = activation_error_handling do
+ require "bundler/version"
+ end
+ return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION))
+ warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`"
+ exit 42
+ end
+
+ def activation_error_handling
+ yield
+ nil
+ rescue StandardError, LoadError => e
+ e
+ end
+end
+
+m.load_bundler!
+
+if m.invoked_as_script?
+ load Gem.bin_path("bundler", "bundle")
+end
diff --git a/bin/guard b/bin/guard
new file mode 100755
index 00000000..ff444e0c
--- /dev/null
+++ b/bin/guard
@@ -0,0 +1,27 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+#
+# This file was generated by Bundler.
+#
+# The application 'guard' is installed as part of a gem, and
+# this file is here to facilitate running it.
+#
+
+ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
+
+bundle_binstub = File.expand_path("bundle", __dir__)
+
+if File.file?(bundle_binstub)
+ if File.read(bundle_binstub, 300).include?("This file was generated by Bundler")
+ load(bundle_binstub)
+ else
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
+Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
+ end
+end
+
+require "rubygems"
+require "bundler/setup"
+
+load Gem.bin_path("guard", "guard")
diff --git a/bin/rackup b/bin/rackup
new file mode 100755
index 00000000..6408c791
--- /dev/null
+++ b/bin/rackup
@@ -0,0 +1,27 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+#
+# This file was generated by Bundler.
+#
+# The application 'rackup' is installed as part of a gem, and
+# this file is here to facilitate running it.
+#
+
+ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
+
+bundle_binstub = File.expand_path("bundle", __dir__)
+
+if File.file?(bundle_binstub)
+ if File.read(bundle_binstub, 300).include?("This file was generated by Bundler")
+ load(bundle_binstub)
+ else
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
+Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
+ end
+end
+
+require "rubygems"
+require "bundler/setup"
+
+load Gem.bin_path("rackup", "rackup")
diff --git a/bin/rake b/bin/rake
new file mode 100755
index 00000000..4eb7d7bf
--- /dev/null
+++ b/bin/rake
@@ -0,0 +1,27 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+#
+# This file was generated by Bundler.
+#
+# The application 'rake' is installed as part of a gem, and
+# this file is here to facilitate running it.
+#
+
+ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
+
+bundle_binstub = File.expand_path("bundle", __dir__)
+
+if File.file?(bundle_binstub)
+ if File.read(bundle_binstub, 300).include?("This file was generated by Bundler")
+ load(bundle_binstub)
+ else
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
+Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
+ end
+end
+
+require "rubygems"
+require "bundler/setup"
+
+load Gem.bin_path("rake", "rake")
diff --git a/bin/rspec b/bin/rspec
new file mode 100755
index 00000000..cb53ebe5
--- /dev/null
+++ b/bin/rspec
@@ -0,0 +1,27 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+#
+# This file was generated by Bundler.
+#
+# The application 'rspec' is installed as part of a gem, and
+# this file is here to facilitate running it.
+#
+
+ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
+
+bundle_binstub = File.expand_path("bundle", __dir__)
+
+if File.file?(bundle_binstub)
+ if File.read(bundle_binstub, 300).include?("This file was generated by Bundler")
+ load(bundle_binstub)
+ else
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
+Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
+ end
+end
+
+require "rubygems"
+require "bundler/setup"
+
+load Gem.bin_path("rspec-core", "rspec")
diff --git a/bin/rubocop b/bin/rubocop
new file mode 100755
index 00000000..369a05be
--- /dev/null
+++ b/bin/rubocop
@@ -0,0 +1,27 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+#
+# This file was generated by Bundler.
+#
+# The application 'rubocop' is installed as part of a gem, and
+# this file is here to facilitate running it.
+#
+
+ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
+
+bundle_binstub = File.expand_path("bundle", __dir__)
+
+if File.file?(bundle_binstub)
+ if File.read(bundle_binstub, 300).include?("This file was generated by Bundler")
+ load(bundle_binstub)
+ else
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
+Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
+ end
+end
+
+require "rubygems"
+require "bundler/setup"
+
+load Gem.bin_path("rubocop", "rubocop")
diff --git a/ci/network/admin/tnsnames.ora b/ci/network/admin/tnsnames.ora
new file mode 100644
index 00000000..d1ba8183
--- /dev/null
+++ b/ci/network/admin/tnsnames.ora
@@ -0,0 +1,15 @@
+FREEPDB1 =
+ (DESCRIPTION =
+ (ADDRESS = (PROTOCOL = TCP)(HOST = 127.0.0.1)(PORT = 1521))
+ (CONNECT_DATA =
+ (SERVICE_NAME = FREEPDB1)
+ )
+ )
+
+XE =
+ (DESCRIPTION =
+ (ADDRESS = (PROTOCOL = TCP)(HOST = 127.0.0.1)(PORT = 1521))
+ (CONNECT_DATA =
+ (SERVICE_NAME = XE)
+ )
+ )
diff --git a/ci/setup_accounts.sh b/ci/setup_accounts.sh
new file mode 100755
index 00000000..3ed4d2ff
--- /dev/null
+++ b/ci/setup_accounts.sh
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+
+set -ev
+
+sqlplus sys/${DATABASE_SYS_PASSWORD}@${DATABASE_NAME} as sysdba< 0.3.18"
-gem "activerecord-oracle_enhanced-adapter", "~> 1.5.0"
-gem "ruby-oci8" if ENV["DB_ADAPTER"] == "oracle_enhanced"
-
-group :test do
- gem "codeclimate-test-reporter", "~> 1.0.0"
-end
-
-gemspec path: "../"
diff --git a/gemfiles/rails_4.1.15.gemfile b/gemfiles/rails_4.1.15.gemfile
deleted file mode 100644
index 0b8a98ef..00000000
--- a/gemfiles/rails_4.1.15.gemfile
+++ /dev/null
@@ -1,14 +0,0 @@
-# This file was generated by Appraisal
-
-source "https://rubygems.org/"
-
-gem "rails", "4.1.15"
-gem "mysql2", "~> 0.3.18"
-gem "activerecord-oracle_enhanced-adapter", "~> 1.5.0"
-gem "ruby-oci8" if ENV["DB_ADAPTER"] == "oracle_enhanced"
-
-group :test do
- gem "codeclimate-test-reporter", "~> 1.0.0"
-end
-
-gemspec path: "../"
diff --git a/gemfiles/rails_4.2.9.gemfile b/gemfiles/rails_4.2.9.gemfile
deleted file mode 100644
index 261f43d3..00000000
--- a/gemfiles/rails_4.2.9.gemfile
+++ /dev/null
@@ -1,13 +0,0 @@
-# This file was generated by Appraisal
-
-source "https://rubygems.org/"
-
-gem "rails", "4.2.9"
-gem "activerecord-oracle_enhanced-adapter", "~> 1.6.0"
-gem "ruby-oci8" if ENV["DB_ADAPTER"] == "oracle_enhanced"
-
-group :test do
- gem "codeclimate-test-reporter", "~> 1.0.0"
-end
-
-gemspec path: "../"
diff --git a/gemfiles/rails_5.0.4.gemfile b/gemfiles/rails_5.0.4.gemfile
deleted file mode 100644
index 4822c1ee..00000000
--- a/gemfiles/rails_5.0.4.gemfile
+++ /dev/null
@@ -1,13 +0,0 @@
-# This file was generated by Appraisal
-
-source "https://rubygems.org/"
-
-gem "rails", "5.0.4"
-gem "activerecord-oracle_enhanced-adapter", "~> 1.7.0"
-gem "ruby-oci8" if ENV["DB_ADAPTER"] == "oracle_enhanced"
-
-group :test do
- gem "codeclimate-test-reporter", "~> 1.0.0"
-end
-
-gemspec path: "../"
diff --git a/gemfiles/rails_5.1.2.gemfile b/gemfiles/rails_5.1.2.gemfile
deleted file mode 100644
index 3892ab26..00000000
--- a/gemfiles/rails_5.1.2.gemfile
+++ /dev/null
@@ -1,13 +0,0 @@
-# This file was generated by Appraisal
-
-source "https://rubygems.org/"
-
-gem "rails", "5.1.2"
-gem "activerecord-oracle_enhanced-adapter", "~> 1.8.0"
-gem "ruby-oci8" if ENV["DB_ADAPTER"] == "oracle_enhanced"
-
-group :test do
- gem "codeclimate-test-reporter", "~> 1.0.0"
-end
-
-gemspec path: "../"
diff --git a/gemfiles/rails_7.1_with_mysql2.gemfile b/gemfiles/rails_7.1_with_mysql2.gemfile
new file mode 100644
index 00000000..168f0c11
--- /dev/null
+++ b/gemfiles/rails_7.1_with_mysql2.gemfile
@@ -0,0 +1,25 @@
+# This file was generated by Appraisal
+
+source "https://rubygems.org/"
+
+gem "appraisal", git: "https://github.com/thoughtbot/appraisal.git"
+gem "combustion"
+gem "database_cleaner"
+gem "factory_bot"
+gem "faker"
+gem "generator_spec"
+gem "puma"
+gem "rake"
+gem "rspec"
+gem "rspec-rebound"
+gem "simplecov"
+gem "guard-rspec", require: false
+gem "rubocop", require: false
+gem "rubocop-factory_bot", require: false
+gem "rubocop-performance", require: false
+gem "rubocop-rake", require: false
+gem "rubocop-rspec", require: false
+gem "rails", "~> 7.1.0"
+gem "mysql2"
+
+gemspec path: "../"
diff --git a/gemfiles/rails_7.1_with_oracle_enhanced.gemfile b/gemfiles/rails_7.1_with_oracle_enhanced.gemfile
new file mode 100644
index 00000000..20e36feb
--- /dev/null
+++ b/gemfiles/rails_7.1_with_oracle_enhanced.gemfile
@@ -0,0 +1,25 @@
+# This file was generated by Appraisal
+
+source "https://rubygems.org/"
+
+gem "appraisal", git: "https://github.com/thoughtbot/appraisal.git"
+gem "combustion"
+gem "database_cleaner"
+gem "factory_bot"
+gem "faker"
+gem "generator_spec"
+gem "puma"
+gem "rake"
+gem "rspec"
+gem "rspec-rebound"
+gem "simplecov"
+gem "guard-rspec", require: false
+gem "rubocop", require: false
+gem "rubocop-factory_bot", require: false
+gem "rubocop-performance", require: false
+gem "rubocop-rake", require: false
+gem "rubocop-rspec", require: false
+gem "rails", "~> 7.1.0"
+gem "activerecord-oracle_enhanced-adapter", "~> 7.1.0"
+
+gemspec path: "../"
diff --git a/gemfiles/rails_7.1_with_postgis.gemfile b/gemfiles/rails_7.1_with_postgis.gemfile
new file mode 100644
index 00000000..699d0c5e
--- /dev/null
+++ b/gemfiles/rails_7.1_with_postgis.gemfile
@@ -0,0 +1,26 @@
+# This file was generated by Appraisal
+
+source "https://rubygems.org/"
+
+gem "appraisal", git: "https://github.com/thoughtbot/appraisal.git"
+gem "combustion"
+gem "database_cleaner"
+gem "factory_bot"
+gem "faker"
+gem "generator_spec"
+gem "puma"
+gem "rake"
+gem "rspec"
+gem "rspec-rebound"
+gem "simplecov"
+gem "pg"
+gem "guard-rspec", require: false
+gem "rubocop", require: false
+gem "rubocop-factory_bot", require: false
+gem "rubocop-performance", require: false
+gem "rubocop-rake", require: false
+gem "rubocop-rspec", require: false
+gem "rails", "~> 7.1.0"
+gem "activerecord-postgis-adapter", "~> 9.0.0"
+
+gemspec path: "../"
diff --git a/gemfiles/rails_7.1_with_postgresql.gemfile b/gemfiles/rails_7.1_with_postgresql.gemfile
new file mode 100644
index 00000000..6dbde4a8
--- /dev/null
+++ b/gemfiles/rails_7.1_with_postgresql.gemfile
@@ -0,0 +1,25 @@
+# This file was generated by Appraisal
+
+source "https://rubygems.org/"
+
+gem "appraisal", git: "https://github.com/thoughtbot/appraisal.git"
+gem "combustion"
+gem "database_cleaner"
+gem "factory_bot"
+gem "faker"
+gem "generator_spec"
+gem "puma"
+gem "rake"
+gem "rspec"
+gem "rspec-rebound"
+gem "simplecov"
+gem "pg"
+gem "guard-rspec", require: false
+gem "rubocop", require: false
+gem "rubocop-factory_bot", require: false
+gem "rubocop-performance", require: false
+gem "rubocop-rake", require: false
+gem "rubocop-rspec", require: false
+gem "rails", "~> 7.1.0"
+
+gemspec path: "../"
diff --git a/gemfiles/rails_7.1_with_sqlite3.gemfile b/gemfiles/rails_7.1_with_sqlite3.gemfile
new file mode 100644
index 00000000..7641ede3
--- /dev/null
+++ b/gemfiles/rails_7.1_with_sqlite3.gemfile
@@ -0,0 +1,25 @@
+# This file was generated by Appraisal
+
+source "https://rubygems.org/"
+
+gem "appraisal", git: "https://github.com/thoughtbot/appraisal.git"
+gem "combustion"
+gem "database_cleaner"
+gem "factory_bot"
+gem "faker"
+gem "generator_spec"
+gem "puma"
+gem "rake"
+gem "rspec"
+gem "rspec-rebound"
+gem "simplecov"
+gem "guard-rspec", require: false
+gem "rubocop", require: false
+gem "rubocop-factory_bot", require: false
+gem "rubocop-performance", require: false
+gem "rubocop-rake", require: false
+gem "rubocop-rspec", require: false
+gem "rails", "~> 7.1.0"
+gem "sqlite3", "~> 1.5.0"
+
+gemspec path: "../"
diff --git a/gemfiles/rails_7.1_with_trilogy.gemfile b/gemfiles/rails_7.1_with_trilogy.gemfile
new file mode 100644
index 00000000..a62a7c41
--- /dev/null
+++ b/gemfiles/rails_7.1_with_trilogy.gemfile
@@ -0,0 +1,25 @@
+# This file was generated by Appraisal
+
+source "https://rubygems.org/"
+
+gem "appraisal", git: "https://github.com/thoughtbot/appraisal.git"
+gem "combustion"
+gem "database_cleaner"
+gem "factory_bot"
+gem "faker"
+gem "generator_spec"
+gem "puma"
+gem "rake"
+gem "rspec"
+gem "rspec-rebound"
+gem "simplecov"
+gem "guard-rspec", require: false
+gem "rubocop", require: false
+gem "rubocop-factory_bot", require: false
+gem "rubocop-performance", require: false
+gem "rubocop-rake", require: false
+gem "rubocop-rspec", require: false
+gem "rails", "~> 7.1.0"
+gem "activerecord-trilogy-adapter"
+
+gemspec path: "../"
diff --git a/gemfiles/rails_7.2_with_mysql2.gemfile b/gemfiles/rails_7.2_with_mysql2.gemfile
new file mode 100644
index 00000000..6b4fa44d
--- /dev/null
+++ b/gemfiles/rails_7.2_with_mysql2.gemfile
@@ -0,0 +1,25 @@
+# This file was generated by Appraisal
+
+source "https://rubygems.org/"
+
+gem "appraisal", git: "https://github.com/thoughtbot/appraisal.git"
+gem "combustion"
+gem "database_cleaner"
+gem "factory_bot"
+gem "faker"
+gem "generator_spec"
+gem "puma"
+gem "rake"
+gem "rspec"
+gem "rspec-rebound"
+gem "simplecov"
+gem "guard-rspec", require: false
+gem "rubocop", require: false
+gem "rubocop-factory_bot", require: false
+gem "rubocop-performance", require: false
+gem "rubocop-rake", require: false
+gem "rubocop-rspec", require: false
+gem "rails", "~> 7.2.0"
+gem "mysql2"
+
+gemspec path: "../"
diff --git a/gemfiles/rails_7.2_with_oracle_enhanced.gemfile b/gemfiles/rails_7.2_with_oracle_enhanced.gemfile
new file mode 100644
index 00000000..c498dab6
--- /dev/null
+++ b/gemfiles/rails_7.2_with_oracle_enhanced.gemfile
@@ -0,0 +1,25 @@
+# This file was generated by Appraisal
+
+source "https://rubygems.org/"
+
+gem "appraisal", git: "https://github.com/thoughtbot/appraisal.git"
+gem "combustion"
+gem "database_cleaner"
+gem "factory_bot"
+gem "faker"
+gem "generator_spec"
+gem "puma"
+gem "rake"
+gem "rspec"
+gem "rspec-rebound"
+gem "simplecov"
+gem "guard-rspec", require: false
+gem "rubocop", require: false
+gem "rubocop-factory_bot", require: false
+gem "rubocop-performance", require: false
+gem "rubocop-rake", require: false
+gem "rubocop-rspec", require: false
+gem "rails", "~> 7.2.0"
+gem "activerecord-oracle_enhanced-adapter", "~> 7.2.0"
+
+gemspec path: "../"
diff --git a/gemfiles/rails_7.2_with_postgis.gemfile b/gemfiles/rails_7.2_with_postgis.gemfile
new file mode 100644
index 00000000..6a62fe31
--- /dev/null
+++ b/gemfiles/rails_7.2_with_postgis.gemfile
@@ -0,0 +1,26 @@
+# This file was generated by Appraisal
+
+source "https://rubygems.org/"
+
+gem "appraisal", git: "https://github.com/thoughtbot/appraisal.git"
+gem "combustion"
+gem "database_cleaner"
+gem "factory_bot"
+gem "faker"
+gem "generator_spec"
+gem "puma"
+gem "rake"
+gem "rspec"
+gem "rspec-rebound"
+gem "simplecov"
+gem "pg"
+gem "guard-rspec", require: false
+gem "rubocop", require: false
+gem "rubocop-factory_bot", require: false
+gem "rubocop-performance", require: false
+gem "rubocop-rake", require: false
+gem "rubocop-rspec", require: false
+gem "rails", "~> 7.2.0"
+gem "activerecord-postgis-adapter", "~> 10.0.0"
+
+gemspec path: "../"
diff --git a/gemfiles/rails_7.2_with_postgresql.gemfile b/gemfiles/rails_7.2_with_postgresql.gemfile
new file mode 100644
index 00000000..6c118407
--- /dev/null
+++ b/gemfiles/rails_7.2_with_postgresql.gemfile
@@ -0,0 +1,25 @@
+# This file was generated by Appraisal
+
+source "https://rubygems.org/"
+
+gem "appraisal", git: "https://github.com/thoughtbot/appraisal.git"
+gem "combustion"
+gem "database_cleaner"
+gem "factory_bot"
+gem "faker"
+gem "generator_spec"
+gem "puma"
+gem "rake"
+gem "rspec"
+gem "rspec-rebound"
+gem "simplecov"
+gem "pg"
+gem "guard-rspec", require: false
+gem "rubocop", require: false
+gem "rubocop-factory_bot", require: false
+gem "rubocop-performance", require: false
+gem "rubocop-rake", require: false
+gem "rubocop-rspec", require: false
+gem "rails", "~> 7.2.0"
+
+gemspec path: "../"
diff --git a/gemfiles/rails_7.2_with_sqlite3.gemfile b/gemfiles/rails_7.2_with_sqlite3.gemfile
new file mode 100644
index 00000000..8ee69bab
--- /dev/null
+++ b/gemfiles/rails_7.2_with_sqlite3.gemfile
@@ -0,0 +1,25 @@
+# This file was generated by Appraisal
+
+source "https://rubygems.org/"
+
+gem "appraisal", git: "https://github.com/thoughtbot/appraisal.git"
+gem "combustion"
+gem "database_cleaner"
+gem "factory_bot"
+gem "faker"
+gem "generator_spec"
+gem "puma"
+gem "rake"
+gem "rspec"
+gem "rspec-rebound"
+gem "simplecov"
+gem "guard-rspec", require: false
+gem "rubocop", require: false
+gem "rubocop-factory_bot", require: false
+gem "rubocop-performance", require: false
+gem "rubocop-rake", require: false
+gem "rubocop-rspec", require: false
+gem "rails", "~> 7.2.0"
+gem "sqlite3", "~> 1.5.0"
+
+gemspec path: "../"
diff --git a/gemfiles/rails_7.2_with_trilogy.gemfile b/gemfiles/rails_7.2_with_trilogy.gemfile
new file mode 100644
index 00000000..cb3631c2
--- /dev/null
+++ b/gemfiles/rails_7.2_with_trilogy.gemfile
@@ -0,0 +1,25 @@
+# This file was generated by Appraisal
+
+source "https://rubygems.org/"
+
+gem "appraisal", git: "https://github.com/thoughtbot/appraisal.git"
+gem "combustion"
+gem "database_cleaner"
+gem "factory_bot"
+gem "faker"
+gem "generator_spec"
+gem "puma"
+gem "rake"
+gem "rspec"
+gem "rspec-rebound"
+gem "simplecov"
+gem "guard-rspec", require: false
+gem "rubocop", require: false
+gem "rubocop-factory_bot", require: false
+gem "rubocop-performance", require: false
+gem "rubocop-rake", require: false
+gem "rubocop-rspec", require: false
+gem "rails", "~> 7.2.0"
+gem "activerecord-trilogy-adapter"
+
+gemspec path: "../"
diff --git a/gemfiles/rails_8.0_with_mysql2.gemfile b/gemfiles/rails_8.0_with_mysql2.gemfile
new file mode 100644
index 00000000..765f4597
--- /dev/null
+++ b/gemfiles/rails_8.0_with_mysql2.gemfile
@@ -0,0 +1,25 @@
+# This file was generated by Appraisal
+
+source "https://rubygems.org/"
+
+gem "appraisal", git: "https://github.com/thoughtbot/appraisal.git"
+gem "combustion"
+gem "database_cleaner"
+gem "factory_bot"
+gem "faker"
+gem "generator_spec"
+gem "puma"
+gem "rake"
+gem "rspec"
+gem "rspec-rebound"
+gem "simplecov"
+gem "guard-rspec", require: false
+gem "rubocop", require: false
+gem "rubocop-factory_bot", require: false
+gem "rubocop-performance", require: false
+gem "rubocop-rake", require: false
+gem "rubocop-rspec", require: false
+gem "rails", "~> 8.0.0"
+gem "mysql2"
+
+gemspec path: "../"
diff --git a/gemfiles/rails_8.0_with_oracle_enhanced.gemfile b/gemfiles/rails_8.0_with_oracle_enhanced.gemfile
new file mode 100644
index 00000000..e7e3681d
--- /dev/null
+++ b/gemfiles/rails_8.0_with_oracle_enhanced.gemfile
@@ -0,0 +1,25 @@
+# This file was generated by Appraisal
+
+source "https://rubygems.org/"
+
+gem "appraisal", git: "https://github.com/thoughtbot/appraisal.git"
+gem "combustion"
+gem "database_cleaner"
+gem "factory_bot"
+gem "faker"
+gem "generator_spec"
+gem "puma"
+gem "rake"
+gem "rspec"
+gem "rspec-rebound"
+gem "simplecov"
+gem "guard-rspec", require: false
+gem "rubocop", require: false
+gem "rubocop-factory_bot", require: false
+gem "rubocop-performance", require: false
+gem "rubocop-rake", require: false
+gem "rubocop-rspec", require: false
+gem "rails", "~> 8.0.0"
+gem "activerecord-oracle_enhanced-adapter", "~> 8.0.0"
+
+gemspec path: "../"
diff --git a/gemfiles/rails_8.0_with_postgis.gemfile b/gemfiles/rails_8.0_with_postgis.gemfile
new file mode 100644
index 00000000..e0371b93
--- /dev/null
+++ b/gemfiles/rails_8.0_with_postgis.gemfile
@@ -0,0 +1,26 @@
+# This file was generated by Appraisal
+
+source "https://rubygems.org/"
+
+gem "appraisal", git: "https://github.com/thoughtbot/appraisal.git"
+gem "combustion"
+gem "database_cleaner"
+gem "factory_bot"
+gem "faker"
+gem "generator_spec"
+gem "puma"
+gem "rake"
+gem "rspec"
+gem "rspec-rebound"
+gem "simplecov"
+gem "pg"
+gem "guard-rspec", require: false
+gem "rubocop", require: false
+gem "rubocop-factory_bot", require: false
+gem "rubocop-performance", require: false
+gem "rubocop-rake", require: false
+gem "rubocop-rspec", require: false
+gem "rails", "~> 8.0.0"
+gem "activerecord-postgis-adapter", "~> 11.0.0"
+
+gemspec path: "../"
diff --git a/gemfiles/rails_8.0_with_postgresql.gemfile b/gemfiles/rails_8.0_with_postgresql.gemfile
new file mode 100644
index 00000000..37cf0343
--- /dev/null
+++ b/gemfiles/rails_8.0_with_postgresql.gemfile
@@ -0,0 +1,25 @@
+# This file was generated by Appraisal
+
+source "https://rubygems.org/"
+
+gem "appraisal", git: "https://github.com/thoughtbot/appraisal.git"
+gem "combustion"
+gem "database_cleaner"
+gem "factory_bot"
+gem "faker"
+gem "generator_spec"
+gem "puma"
+gem "rake"
+gem "rspec"
+gem "rspec-rebound"
+gem "simplecov"
+gem "pg"
+gem "guard-rspec", require: false
+gem "rubocop", require: false
+gem "rubocop-factory_bot", require: false
+gem "rubocop-performance", require: false
+gem "rubocop-rake", require: false
+gem "rubocop-rspec", require: false
+gem "rails", "~> 8.0.0"
+
+gemspec path: "../"
diff --git a/gemfiles/rails_8.0_with_sqlite3.gemfile b/gemfiles/rails_8.0_with_sqlite3.gemfile
new file mode 100644
index 00000000..cdc0a53b
--- /dev/null
+++ b/gemfiles/rails_8.0_with_sqlite3.gemfile
@@ -0,0 +1,25 @@
+# This file was generated by Appraisal
+
+source "https://rubygems.org/"
+
+gem "appraisal", git: "https://github.com/thoughtbot/appraisal.git"
+gem "combustion"
+gem "database_cleaner"
+gem "factory_bot"
+gem "faker"
+gem "generator_spec"
+gem "puma"
+gem "rake"
+gem "rspec"
+gem "rspec-rebound"
+gem "simplecov"
+gem "guard-rspec", require: false
+gem "rubocop", require: false
+gem "rubocop-factory_bot", require: false
+gem "rubocop-performance", require: false
+gem "rubocop-rake", require: false
+gem "rubocop-rspec", require: false
+gem "rails", "~> 8.0.0"
+gem "sqlite3"
+
+gemspec path: "../"
diff --git a/gemfiles/rails_8.0_with_trilogy.gemfile b/gemfiles/rails_8.0_with_trilogy.gemfile
new file mode 100644
index 00000000..c114794e
--- /dev/null
+++ b/gemfiles/rails_8.0_with_trilogy.gemfile
@@ -0,0 +1,25 @@
+# This file was generated by Appraisal
+
+source "https://rubygems.org/"
+
+gem "appraisal", git: "https://github.com/thoughtbot/appraisal.git"
+gem "combustion"
+gem "database_cleaner"
+gem "factory_bot"
+gem "faker"
+gem "generator_spec"
+gem "puma"
+gem "rake"
+gem "rspec"
+gem "rspec-rebound"
+gem "simplecov"
+gem "guard-rspec", require: false
+gem "rubocop", require: false
+gem "rubocop-factory_bot", require: false
+gem "rubocop-performance", require: false
+gem "rubocop-rake", require: false
+gem "rubocop-rspec", require: false
+gem "rails", "~> 8.0.0"
+gem "activerecord-trilogy-adapter"
+
+gemspec path: "../"
diff --git a/lib/ajax-datatables-rails.rb b/lib/ajax-datatables-rails.rb
index a4662a42..cb9f515f 100644
--- a/lib/ajax-datatables-rails.rb
+++ b/lib/ajax-datatables-rails.rb
@@ -1,11 +1,17 @@
+# frozen_string_literal: true
+
+# require external dependencies
+require 'zeitwerk'
+
+# load zeitwerk
+Zeitwerk::Loader.for_gem.tap do |loader|
+ loader.ignore("#{__dir__}/generators")
+ loader.inflector.inflect(
+ 'orm' => 'ORM',
+ 'ajax-datatables-rails' => 'AjaxDatatablesRails'
+ )
+ loader.setup
+end
+
module AjaxDatatablesRails
- require 'ajax-datatables-rails/version'
- require 'ajax-datatables-rails/config'
- require 'ajax-datatables-rails/base'
- require 'ajax-datatables-rails/datatable/datatable'
- require 'ajax-datatables-rails/datatable/simple_search'
- require 'ajax-datatables-rails/datatable/simple_order'
- require 'ajax-datatables-rails/datatable/column_date_filter' unless AjaxDatatablesRails.old_rails?
- require 'ajax-datatables-rails/datatable/column'
- require 'ajax-datatables-rails/orm/active_record'
end
diff --git a/lib/ajax-datatables-rails/active_record.rb b/lib/ajax-datatables-rails/active_record.rb
new file mode 100644
index 00000000..2f062e07
--- /dev/null
+++ b/lib/ajax-datatables-rails/active_record.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module AjaxDatatablesRails
+ class ActiveRecord < AjaxDatatablesRails::Base
+ include AjaxDatatablesRails::ORM::ActiveRecord
+ end
+end
diff --git a/lib/ajax-datatables-rails/base.rb b/lib/ajax-datatables-rails/base.rb
index 6869ab53..4d52feee 100644
--- a/lib/ajax-datatables-rails/base.rb
+++ b/lib/ajax-datatables-rails/base.rb
@@ -1,87 +1,133 @@
+# frozen_string_literal: true
+
module AjaxDatatablesRails
- class Base
- extend Forwardable
+ class Base # rubocop:disable Metrics/ClassLength
- attr_reader :view, :options
- def_delegator :@view, :params
+ class << self
+ def default_db_adapter
+ ::ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).first.adapter.downcase.to_sym
+ end
+ end
- GLOBAL_SEARCH_DELIMITER = ' '.freeze
+ class_attribute :db_adapter, default: default_db_adapter
+ class_attribute :nulls_last, default: false
- def initialize(view, options = {})
- @view = view
- @options = options
- load_orm_extension
- end
+ attr_reader :params, :options, :datatable
- def config
- @config ||= AjaxDatatablesRails.config
- end
+ GLOBAL_SEARCH_DELIMITER = ' '
+
+ def initialize(params, options = {})
+ @params = params
+ @options = options
+ @datatable = Datatable::Datatable.new(self)
- def datatable
- @datatable ||= Datatable::Datatable.new(self)
+ @view_columns = nil
+ @connected_columns = nil
+ @searchable_columns = nil
+ @search_columns = nil
+ @records = nil
+ @build_conditions = nil
end
+ # User defined methods
def view_columns
- fail(NotImplementedError, view_columns_error_text)
+ raise(NotImplementedError, <<~ERROR)
+
+ You should implement this method in your class and return an array
+ of database columns based on the columns displayed in the HTML view.
+ These columns should be represented in the ModelName.column_name,
+ or aliased_join_table.column_name notation.
+ ERROR
end
- def get_raw_records
- fail(NotImplementedError, raw_records_error_text)
+ def get_raw_records # rubocop:disable Naming/AccessorMethodName
+ raise(NotImplementedError, <<~ERROR)
+
+ You should implement this method in your class and specify
+ how records are going to be retrieved from the database.
+ ERROR
end
def data
- fail(NotImplementedError, data_error_text)
+ raise(NotImplementedError, <<~ERROR)
+
+ You should implement this method in your class and return an array
+ of arrays, or an array of hashes, as defined in the jQuery.dataTables
+ plugin documentation.
+ ERROR
+ end
+
+ # ORM defined methods
+ def fetch_records
+ get_raw_records
+ end
+
+ def filter_records(records)
+ raise(NotImplementedError)
end
- def additional_datas
+ def sort_records(records)
+ raise(NotImplementedError)
+ end
+
+ def paginate_records(records)
+ raise(NotImplementedError)
+ end
+
+ # User overides
+ def additional_data
{}
end
+ # JSON structure sent to jQuery DataTables
def as_json(*)
{
- recordsTotal: records_total_count,
+ recordsTotal: records_total_count,
recordsFiltered: records_filtered_count,
- data: sanitize(data)
- }.merge(additional_datas)
+ data: sanitize_data(data),
+ }.merge(draw_id).merge(additional_data)
end
- def records
- @records ||= retrieve_records
+ # User helper methods
+ def column_id(name)
+ view_columns.keys.index(name.to_sym)
end
+ def column_data(column)
+ id = column_id(column)
+ params.dig('columns', id.to_s, 'search', 'value')
+ end
+
+ private
+
# helper methods
def connected_columns
- @connected_columns ||= begin
- view_columns.keys.map do |field_name|
- datatable.column_by(:data, field_name.to_s)
- end.compact
- end
+ @connected_columns ||= view_columns.keys.filter_map { |field_name| datatable.column_by(:data, field_name.to_s) }
end
def searchable_columns
- @searchable_columns ||= begin
- connected_columns.select(&:searchable?)
- end
+ @searchable_columns ||= connected_columns.select(&:searchable?)
end
def search_columns
- @search_columns ||= begin
- searchable_columns.select(&:searched?)
- end
+ @search_columns ||= searchable_columns.select(&:searched?)
end
- private
-
- def sanitize(data)
+ def sanitize_data(data)
data.map do |record|
if record.is_a?(Array)
record.map { |td| ERB::Util.html_escape(td) }
else
- record.update(record){ |_, v| ERB::Util.html_escape(v) }
+ record.update(record) { |_, v| ERB::Util.html_escape(v) }
end
end
end
+ # called from within #data
+ def records
+ @records ||= retrieve_records
+ end
+
def retrieve_records
records = fetch_records
records = filter_records(records)
@@ -91,52 +137,25 @@ def retrieve_records
end
def records_total_count
- get_raw_records.count(:all)
+ numeric_count fetch_records.count(:all)
end
def records_filtered_count
- filter_records(get_raw_records).count(:all)
+ numeric_count filter_records(fetch_records).count(:all)
end
- # Private helper methods
- def load_orm_extension
- case config.orm
- when :mongoid then nil
- when :active_record then extend ORM::ActiveRecord
- else
- nil
- end
+ def numeric_count(count)
+ count.is_a?(Hash) ? count.values.size : count
end
def global_search_delimiter
GLOBAL_SEARCH_DELIMITER
end
- def raw_records_error_text
- return <<-eos
-
- You should implement this method in your class and specify
- how records are going to be retrieved from the database.
- eos
- end
-
- def data_error_text
- return <<-eos
-
- You should implement this method in your class and return an array
- of arrays, or an array of hashes, as defined in the jQuery.dataTables
- plugin documentation.
- eos
+ # See: https://datatables.net/manual/server-side#Returned-data
+ def draw_id
+ params[:draw].present? ? { draw: params[:draw].to_i } : {}
end
- def view_columns_error_text
- return <<-eos
-
- You should implement this method in your class and return an array
- of database columns based on the columns displayed in the HTML view.
- These columns should be represented in the ModelName.column_name,
- or aliased_join_table.column_name notation.
- eos
- end
end
end
diff --git a/lib/ajax-datatables-rails/config.rb b/lib/ajax-datatables-rails/config.rb
deleted file mode 100644
index bd252889..00000000
--- a/lib/ajax-datatables-rails/config.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-require 'active_support/configurable'
-
-module AjaxDatatablesRails
-
- # configure AjaxDatatablesRails global settings
- # AjaxDatatablesRails.configure do |config|
- # config.db_adapter = :postgresql
- # end
- def self.configure
- yield @config ||= AjaxDatatablesRails::Configuration.new
- end
-
- # AjaxDatatablesRails global settings
- def self.config
- @config ||= AjaxDatatablesRails::Configuration.new
- end
-
- def self.old_rails?
- Rails::VERSION::MAJOR == 4 && (Rails::VERSION::MINOR == 1 || Rails::VERSION::MINOR == 0)
- end
-
- class Configuration
- include ActiveSupport::Configurable
-
- config_accessor(:orm) { :active_record }
- config_accessor(:db_adapter) { :postgresql }
- end
-end
diff --git a/lib/ajax-datatables-rails/datatable.rb b/lib/ajax-datatables-rails/datatable.rb
new file mode 100644
index 00000000..739e07b0
--- /dev/null
+++ b/lib/ajax-datatables-rails/datatable.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+module AjaxDatatablesRails
+ module Datatable
+ end
+end
diff --git a/lib/ajax-datatables-rails/datatable/column.rb b/lib/ajax-datatables-rails/datatable/column.rb
index 495e875e..2179a4da 100644
--- a/lib/ajax-datatables-rails/datatable/column.rb
+++ b/lib/ajax-datatables-rails/datatable/column.rb
@@ -1,167 +1,131 @@
-require 'ostruct'
+# frozen_string_literal: true
module AjaxDatatablesRails
module Datatable
class Column
- attr_reader :datatable, :index, :options
- unless AjaxDatatablesRails.old_rails?
- prepend ColumnDateFilter
- end
-
- def initialize(datatable, index, options)
- @datatable, @index, @options = datatable, index, options
- @view_column = datatable.view_columns[options["data"].to_sym]
- end
-
- def data
- options[:data].presence || options[:name]
- end
-
- def searchable?
- @view_column.fetch(:searchable, true)
- end
+ include Search
+ include Order
+ include DateFilter
- def orderable?
- @view_column.fetch(:orderable, true)
- end
+ attr_reader :datatable, :index, :options, :column_name
+ attr_writer :search
- def search
- @search ||= SimpleSearch.new(options[:search])
- end
+ def initialize(datatable, index, options) # rubocop:disable Metrics/MethodLength
+ @datatable = datatable
+ @index = index
+ @options = options
+ @column_name = options[:data]&.to_sym
+ @view_column = datatable.view_columns[@column_name]
- def searched?
- search.value.present?
- end
-
- def search=(value)
- @search = value
- end
+ @model = nil
+ @field = nil
+ @type_cast = nil
+ @casted_column = nil
+ @search = nil
+ @delimiter = nil
+ @range_start = nil
+ @range_end = nil
- def cond
- @view_column[:cond] || :like
+ validate_settings!
end
- def filter
- @view_column[:cond].call(self, formated_value)
+ def data
+ options[:data].presence || options[:name]
end
def source
@view_column[:source]
end
- # Add sort_field option to allow overriding of sort field
- def sort_field
- @view_column[:sort_field] || field
- end
-
- # Add formater option to allow modification of the value
- # before passing it to the database
- def formater
- @view_column[:formater]
- end
-
- # Add use_regex option to allow bypassing of regex search
- def use_regex?
- @view_column.fetch(:use_regex, true)
- end
-
- # Add delimiter option to handle range search
- def delimiter
- @view_column[:delimiter] || '-'
+ def table
+ model.respond_to?(:arel_table) ? model.arel_table : model
end
- def table
- model = source.split('.').first.constantize
- model.arel_table rescue model
+ def model
+ @model ||= custom_field? ? source : source.split('.').first.constantize
end
def field
- source.split('.').last.to_sym
+ @field ||= custom_field? ? source : source.split('.').last.to_sym
end
- def search_query
- search.regexp? ? regex_search : non_regex_search
+ def custom_field?
+ !source.include?('.')
end
- def sort_query
- custom_field? ? source : "#{table.name}.#{sort_field}"
+ # Add formatter option to allow modification of the value
+ # before passing it to the database
+ def formatter
+ @view_column[:formatter]
end
- def formated_value
- formater ? formater.call(search.value) : search.value
+ def formatted_value
+ formatter ? formatter.call(search.value) : search.value
end
private
- def custom_field?
- !source.include?('.')
+ TYPE_CAST_DEFAULT = 'VARCHAR'
+ TYPE_CAST_MYSQL = 'CHAR'
+ TYPE_CAST_SQLITE = 'TEXT'
+ TYPE_CAST_ORACLE = 'VARCHAR2(4000)'
+ TYPE_CAST_SQLSERVER = 'VARCHAR(4000)'
+
+ DB_ADAPTER_TYPE_CAST = {
+ mysql: TYPE_CAST_MYSQL,
+ mysql2: TYPE_CAST_MYSQL,
+ trilogy: TYPE_CAST_MYSQL,
+ sqlite: TYPE_CAST_SQLITE,
+ sqlite3: TYPE_CAST_SQLITE,
+ oracle: TYPE_CAST_ORACLE,
+ oracleenhanced: TYPE_CAST_ORACLE,
+ oracle_enhanced: TYPE_CAST_ORACLE,
+ sqlserver: TYPE_CAST_SQLSERVER,
+ }.freeze
+
+ private_constant :TYPE_CAST_DEFAULT
+ private_constant :TYPE_CAST_MYSQL
+ private_constant :TYPE_CAST_SQLITE
+ private_constant :TYPE_CAST_ORACLE
+ private_constant :TYPE_CAST_SQLSERVER
+ private_constant :DB_ADAPTER_TYPE_CAST
+
+ def type_cast
+ @type_cast ||= DB_ADAPTER_TYPE_CAST.fetch(datatable.db_adapter, TYPE_CAST_DEFAULT)
end
- def config
- @config ||= AjaxDatatablesRails.config
+ def casted_column
+ @casted_column ||= ::Arel::Nodes::NamedFunction.new('CAST', [table[field].as(type_cast)])
end
- # Using multi-select filters in JQuery Datatable auto-enables regex_search.
- # Unfortunately regex_search doesn't work when filtering on primary keys with integer.
- # It generates this kind of query : AND ("regions"."id" ~ '2|3') which throws an error :
- # operator doesn't exist : integer ~ unknown
- # The solution is to bypass regex_search and use non_regex_search with :in operator
- def regex_search
- if use_regex?
- ::Arel::Nodes::Regexp.new((custom_field? ? field : table[field]), ::Arel::Nodes.build_quoted(formated_value))
- else
- non_regex_search
- end
+ # rubocop:disable Layout/LineLength
+ def validate_settings!
+ raise AjaxDatatablesRails::Error::InvalidSearchColumn, 'Unknown column. Check that `data` field is filled on JS side with the column name' if column_name.empty?
+ raise AjaxDatatablesRails::Error::InvalidSearchColumn, "Check that column '#{column_name}' exists in view_columns" unless valid_search_column?(column_name)
+ raise AjaxDatatablesRails::Error::InvalidSearchCondition, cond unless valid_search_condition?(cond)
end
+ # rubocop:enable Layout/LineLength
- def non_regex_search
- case cond
- when Proc
- filter
- when :eq, :not_eq, :lt, :gt, :lteq, :gteq, :in
- numeric_search
- when :null_value
- null_value_search
- when :start_with
- casted_column.matches("#{formated_value}%")
- when :end_with
- casted_column.matches("%#{formated_value}")
- when :like
- casted_column.matches("%#{formated_value}%")
- else
- nil
- end
+ def valid_search_column?(column_name)
+ !datatable.view_columns[column_name].nil?
end
- def typecast
- case config.db_adapter
- when :oracle, :oracleenhanced then 'VARCHAR2(4000)'
- when :mysql, :mysql2 then 'CHAR'
- when :sqlite, :sqlite3 then 'TEXT'
- else
- 'VARCHAR'
- end
- end
+ VALID_SEARCH_CONDITIONS = [
+ # String condition
+ :start_with, :end_with, :like, :string_eq, :string_in, :null_value,
+ # Numeric condition
+ :eq, :not_eq, :lt, :gt, :lteq, :gteq, :in,
+ # Date condition
+ :date_range
+ ].freeze
- def casted_column
- ::Arel::Nodes::NamedFunction.new('CAST', [table[field].as(typecast)])
- end
+ private_constant :VALID_SEARCH_CONDITIONS
- def null_value_search
- if formated_value == '!NULL'
- table[field].not_eq(nil)
- else
- table[field].eq(nil)
- end
- end
+ def valid_search_condition?(cond)
+ return true if cond.is_a?(Proc)
- def numeric_search
- if custom_field?
- ::Arel::Nodes::SqlLiteral.new(field).eq(formated_value)
- else
- table[field].send(cond, formated_value)
- end
+ VALID_SEARCH_CONDITIONS.include?(cond)
end
end
diff --git a/lib/ajax-datatables-rails/datatable/column/date_filter.rb b/lib/ajax-datatables-rails/datatable/column/date_filter.rb
new file mode 100644
index 00000000..c680f15b
--- /dev/null
+++ b/lib/ajax-datatables-rails/datatable/column/date_filter.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+module AjaxDatatablesRails
+ module Datatable
+ class Column
+ module DateFilter
+
+ RANGE_DELIMITER = '-'
+
+ class DateRange
+ attr_reader :begin, :end
+
+ def initialize(date_start, date_end)
+ @begin = date_start
+ @end = date_end
+ end
+
+ def exclude_end?
+ false
+ end
+ end
+
+ # Add delimiter option to handle range search
+ def delimiter
+ @delimiter ||= @view_column.fetch(:delimiter, RANGE_DELIMITER)
+ end
+
+ # A range value is in form ''
+ # This returns
+ def range_start
+ @range_start ||= formatted_value.split(delimiter)[0]
+ end
+
+ # A range value is in form ''
+ # This returns
+ def range_end
+ @range_end ||= formatted_value.split(delimiter)[1]
+ end
+
+ def empty_range_search?
+ (formatted_value == delimiter) || (range_start.blank? && range_end.blank?)
+ end
+
+ # Do a range search
+ def date_range_search
+ return nil if empty_range_search?
+
+ table[field].between(DateRange.new(range_start_casted, range_end_casted))
+ end
+
+ private
+
+ def range_start_casted
+ range_start.blank? ? parse_date('01/01/1970') : parse_date(range_start)
+ end
+
+ def range_end_casted
+ range_end.blank? ? parse_date('9999-12-31 23:59:59') : parse_date("#{range_end} 23:59:59")
+ end
+
+ def parse_date(date)
+ Time.zone ? Time.zone.parse(date) : Time.parse(date)
+ end
+
+ end
+ end
+ end
+end
diff --git a/lib/ajax-datatables-rails/datatable/column/order.rb b/lib/ajax-datatables-rails/datatable/column/order.rb
new file mode 100644
index 00000000..3806e494
--- /dev/null
+++ b/lib/ajax-datatables-rails/datatable/column/order.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module AjaxDatatablesRails
+ module Datatable
+ class Column
+ module Order
+
+ def orderable?
+ @view_column.fetch(:orderable, true)
+ end
+
+ # Add sort_field option to allow overriding of sort field
+ def sort_field
+ @view_column.fetch(:sort_field, field)
+ end
+
+ def sort_query
+ custom_field? ? source : "#{table.name}.#{sort_field}"
+ end
+
+ # Add option to sort null values last
+ def nulls_last?
+ @view_column.fetch(:nulls_last, false)
+ end
+
+ end
+ end
+ end
+end
diff --git a/lib/ajax-datatables-rails/datatable/column/search.rb b/lib/ajax-datatables-rails/datatable/column/search.rb
new file mode 100644
index 00000000..581165fb
--- /dev/null
+++ b/lib/ajax-datatables-rails/datatable/column/search.rb
@@ -0,0 +1,125 @@
+# frozen_string_literal: true
+
+module AjaxDatatablesRails
+ module Datatable
+ class Column
+ module Search
+
+ SMALLEST_PQ_INTEGER = -2_147_483_648
+ LARGEST_PQ_INTEGER = 2_147_483_647
+ NOT_NULL_VALUE = '!NULL'
+ EMPTY_VALUE = ''
+
+ def searchable?
+ @view_column.fetch(:searchable, true)
+ end
+
+ def cond
+ @view_column.fetch(:cond, :like)
+ end
+
+ def filter
+ @view_column[:cond].call(self, formatted_value)
+ end
+
+ def search
+ @search ||= SimpleSearch.new(options[:search])
+ end
+
+ def searched?
+ search.value.present?
+ end
+
+ def search_query
+ search.regexp? ? regex_search : non_regex_search
+ end
+
+ # Add use_regex option to allow bypassing of regex search
+ def use_regex?
+ @view_column.fetch(:use_regex, true)
+ end
+
+ private
+
+ # Using multi-select filters in JQuery Datatable auto-enables regex_search.
+ # Unfortunately regex_search doesn't work when filtering on primary keys with integer.
+ # It generates this kind of query : AND ("regions"."id" ~ '2|3') which throws an error :
+ # operator doesn't exist : integer ~ unknown
+ # The solution is to bypass regex_search and use non_regex_search with :in operator
+ def regex_search
+ if use_regex?
+ ::Arel::Nodes::Regexp.new((custom_field? ? field : table[field]), ::Arel::Nodes.build_quoted(formatted_value))
+ else
+ non_regex_search
+ end
+ end
+
+ # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity
+ def non_regex_search
+ case cond
+ when Proc
+ filter
+ when :eq, :not_eq, :lt, :gt, :lteq, :gteq, :in
+ searchable_integer? ? raw_search(cond) : empty_search
+ when :start_with
+ text_search("#{formatted_value}%")
+ when :end_with
+ text_search("%#{formatted_value}")
+ when :like
+ text_search("%#{formatted_value}%")
+ when :string_eq
+ raw_search(:eq)
+ when :string_in
+ raw_search(:in)
+ when :null_value
+ null_value_search
+ when :date_range
+ date_range_search
+ end
+ end
+ # rubocop:enable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity
+
+ def null_value_search
+ if formatted_value == NOT_NULL_VALUE
+ table[field].not_eq(nil)
+ else
+ table[field].eq(nil)
+ end
+ end
+
+ def raw_search(cond)
+ table[field].send(cond, formatted_value) unless custom_field?
+ end
+
+ def text_search(value)
+ casted_column.matches(value) unless custom_field?
+ end
+
+ def empty_search
+ casted_column.matches(EMPTY_VALUE)
+ end
+
+ def searchable_integer?
+ if formatted_value.is_a?(Array)
+ valids = formatted_value.map { |v| integer?(v) && !out_of_range?(v) }
+ !valids.include?(false)
+ else
+ integer?(formatted_value) && !out_of_range?(formatted_value)
+ end
+ end
+
+ def out_of_range?(search_value)
+ Integer(search_value) > LARGEST_PQ_INTEGER || Integer(search_value) < SMALLEST_PQ_INTEGER
+ end
+
+ def integer?(string)
+ Integer(string)
+ true
+ rescue ArgumentError
+ false
+ end
+
+ end
+ end
+ end
+end
diff --git a/lib/ajax-datatables-rails/datatable/column_date_filter.rb b/lib/ajax-datatables-rails/datatable/column_date_filter.rb
deleted file mode 100644
index df2b659e..00000000
--- a/lib/ajax-datatables-rails/datatable/column_date_filter.rb
+++ /dev/null
@@ -1,41 +0,0 @@
-module AjaxDatatablesRails
- module Datatable
- module ColumnDateFilter
-
- def empty_range_search?
- (formated_value == delimiter) || (range_start.blank? && range_end.blank?)
- end
-
- # A range value is in form ''
- # This returns
- def range_start
- @range_start ||= formated_value.split(delimiter)[0]
- end
-
- # A range value is in form ''
- # This returns
- def range_end
- @range_end ||= formated_value.split(delimiter)[1]
- end
-
- # Do a range search
- def date_range_search
- return nil if empty_range_search?
- new_start = range_start.blank? ? DateTime.parse('01/01/1970') : DateTime.parse(range_start)
- new_end = range_end.blank? ? DateTime.current : DateTime.parse("#{range_end} 23:59:59")
- table[field].between(OpenStruct.new(begin: new_start, end: new_end))
- end
-
- private
-
- def non_regex_search
- if cond == :date_range
- date_range_search
- else
- super
- end
- end
-
- end
- end
-end
diff --git a/lib/ajax-datatables-rails/datatable/datatable.rb b/lib/ajax-datatables-rails/datatable/datatable.rb
index cdda7e1e..0d4e4d6f 100644
--- a/lib/ajax-datatables-rails/datatable/datatable.rb
+++ b/lib/ajax-datatables-rails/datatable/datatable.rb
@@ -1,14 +1,17 @@
+# frozen_string_literal: true
+
module AjaxDatatablesRails
module Datatable
-
- TRUE_VALUE = 'true'.freeze
-
class Datatable
- attr_reader :datatable, :options
+ attr_reader :options
def initialize(datatable)
@datatable = datatable
@options = datatable.params
+
+ @orders = nil
+ @search = nil
+ @columns = nil
end
# ----------------- ORDER METHODS --------------------
@@ -41,7 +44,7 @@ def search
def columns
@columns ||= get_param(:columns).map do |index, column_options|
- Column.new(datatable, index, column_options)
+ Column.new(@datatable, index, column_options)
end
end
@@ -55,25 +58,38 @@ def paginate?
per_page != -1
end
- def offset
- (page - 1) * per_page
+ def per_page
+ options.fetch(:length, 10).to_i
end
- def page
- (options[:start].to_i / per_page) + 1
+ def offset
+ options.fetch(:start, 0).to_i
end
- def per_page
- options.fetch(:length, 10).to_i
+ def page
+ (offset / per_page) + 1
end
def get_param(param)
- if AjaxDatatablesRails.old_rails?
- options[param]
+ return {} if options[param].nil?
+
+ if options[param].is_a? Array
+ hash = {}
+ options[param].each_with_index { |value, index| hash[index] = value }
+ hash
else
options[param].to_unsafe_h.with_indifferent_access
end
end
+
+ def db_adapter
+ @datatable.db_adapter
+ end
+
+ def nulls_last
+ @datatable.nulls_last
+ end
+
end
end
end
diff --git a/lib/ajax-datatables-rails/datatable/simple_order.rb b/lib/ajax-datatables-rails/datatable/simple_order.rb
index dca4cbfa..b817a586 100644
--- a/lib/ajax-datatables-rails/datatable/simple_order.rb
+++ b/lib/ajax-datatables-rails/datatable/simple_order.rb
@@ -1,16 +1,22 @@
+# frozen_string_literal: true
+
module AjaxDatatablesRails
module Datatable
class SimpleOrder
- DIRECTIONS = %w[DESC ASC].freeze
+ DIRECTION_ASC = 'ASC'
+ DIRECTION_DESC = 'DESC'
+ DIRECTIONS = [DIRECTION_ASC, DIRECTION_DESC].freeze
def initialize(datatable, options = {})
- @datatable = datatable
- @options = options
+ @datatable = datatable
+ @options = options
+ @adapter = datatable.db_adapter
+ @nulls_last = datatable.nulls_last
end
def query(sort_column)
- "#{sort_column} #{direction}"
+ [sort_column, direction, nulls_last_sql].compact.join(' ')
end
def column
@@ -18,7 +24,7 @@ def column
end
def direction
- DIRECTIONS.find { |dir| dir == @options[:dir].upcase } || 'ASC'
+ DIRECTIONS.find { |dir| dir == column_direction } || DIRECTION_ASC
end
private
@@ -26,6 +32,40 @@ def direction
def column_index
@options[:column]
end
+
+ def column_direction
+ @options[:dir].upcase
+ end
+
+ def sort_nulls_last?
+ column.nulls_last? || @nulls_last == true
+ end
+
+ PG_NULL_STYLE = 'NULLS LAST'
+ MYSQL_NULL_STYLE = 'IS NULL'
+ private_constant :PG_NULL_STYLE
+ private_constant :MYSQL_NULL_STYLE
+
+ NULL_STYLE_MAP = {
+ pg: PG_NULL_STYLE,
+ postgresql: PG_NULL_STYLE,
+ postgres: PG_NULL_STYLE,
+ postgis: PG_NULL_STYLE,
+ oracle: PG_NULL_STYLE,
+ mysql: MYSQL_NULL_STYLE,
+ mysql2: MYSQL_NULL_STYLE,
+ trilogy: MYSQL_NULL_STYLE,
+ sqlite: MYSQL_NULL_STYLE,
+ sqlite3: MYSQL_NULL_STYLE,
+ }.freeze
+ private_constant :NULL_STYLE_MAP
+
+ def nulls_last_sql
+ return unless sort_nulls_last?
+
+ NULL_STYLE_MAP[@adapter] || raise("unsupported database adapter: #{@adapter}")
+ end
+
end
end
end
diff --git a/lib/ajax-datatables-rails/datatable/simple_search.rb b/lib/ajax-datatables-rails/datatable/simple_search.rb
index cab3fc35..70bee016 100644
--- a/lib/ajax-datatables-rails/datatable/simple_search.rb
+++ b/lib/ajax-datatables-rails/datatable/simple_search.rb
@@ -1,7 +1,11 @@
+# frozen_string_literal: true
+
module AjaxDatatablesRails
module Datatable
class SimpleSearch
+ TRUE_VALUE = 'true'
+
def initialize(options = {})
@options = options
end
@@ -13,6 +17,7 @@ def value
def regexp?
@options[:regex] == TRUE_VALUE
end
+
end
end
end
diff --git a/lib/ajax-datatables-rails/error.rb b/lib/ajax-datatables-rails/error.rb
new file mode 100644
index 00000000..20fe0d26
--- /dev/null
+++ b/lib/ajax-datatables-rails/error.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module AjaxDatatablesRails
+ module Error
+ class BaseError < StandardError; end
+ class InvalidSearchColumn < BaseError; end
+ class InvalidSearchCondition < BaseError; end
+ end
+end
diff --git a/lib/ajax-datatables-rails/orm.rb b/lib/ajax-datatables-rails/orm.rb
new file mode 100644
index 00000000..9334b847
--- /dev/null
+++ b/lib/ajax-datatables-rails/orm.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+module AjaxDatatablesRails
+ module ORM
+ end
+end
diff --git a/lib/ajax-datatables-rails/orm/active_record.rb b/lib/ajax-datatables-rails/orm/active_record.rb
index 9a252732..8da0895d 100644
--- a/lib/ajax-datatables-rails/orm/active_record.rb
+++ b/lib/ajax-datatables-rails/orm/active_record.rb
@@ -1,22 +1,23 @@
+# frozen_string_literal: true
+
module AjaxDatatablesRails
module ORM
module ActiveRecord
- def fetch_records
- get_raw_records
- end
-
def filter_records(records)
records.where(build_conditions)
end
+ # rubocop:disable Style/EachWithObject, Style/SafeNavigation
def sort_records(records)
sort_by = datatable.orders.inject([]) do |queries, order|
column = order.column
- queries << order.query(column.sort_query) if column
+ queries << order.query(column.sort_query) if column && column.orderable?
+ queries
end
- records.order(sort_by.join(", "))
+ records.order(Arel.sql(sort_by.join(', ')))
end
+ # rubocop:enable Style/EachWithObject, Style/SafeNavigation
def paginate_records(records)
records.offset(datatable.offset).limit(datatable.per_page)
@@ -25,26 +26,25 @@ def paginate_records(records)
# ----------------- SEARCH HELPER METHODS --------------------
def build_conditions
- if datatable.searchable?
- build_conditions_for_datatable
- else
- build_conditions_for_selected_columns
+ @build_conditions ||= begin
+ criteria = [build_conditions_for_selected_columns]
+ criteria << build_conditions_for_datatable if datatable.searchable?
+ criteria.compact.reduce(:and)
end
end
def build_conditions_for_datatable
- criteria = search_for.inject([]) do |crit, atom|
- search = Datatable::SimpleSearch.new({ value: atom, regex: datatable.search.regexp? })
- crit << searchable_columns.map do |simple_column|
- simple_column.search = search
+ columns = searchable_columns.reject(&:searched?)
+ search_for.inject([]) do |crit, atom|
+ crit << columns.filter_map do |simple_column|
+ simple_column.search = Datatable::SimpleSearch.new(value: atom, regex: datatable.search.regexp?)
simple_column.search_query
end.reduce(:or)
end.compact.reduce(:and)
- criteria
end
def build_conditions_for_selected_columns
- search_columns.map(&:search_query).compact.reduce(:and)
+ search_columns.filter_map(&:search_query).reduce(:and)
end
def search_for
diff --git a/lib/ajax-datatables-rails/version.rb b/lib/ajax-datatables-rails/version.rb
index d5472c0c..3e9a1320 100644
--- a/lib/ajax-datatables-rails/version.rb
+++ b/lib/ajax-datatables-rails/version.rb
@@ -1,3 +1,17 @@
+# frozen_string_literal: true
+
module AjaxDatatablesRails
- VERSION = '0.4.0'.freeze
+
+ def self.gem_version
+ Gem::Version.new VERSION::STRING
+ end
+
+ module VERSION
+ MAJOR = 1
+ MINOR = 5
+ TINY = 0
+ PRE = nil
+
+ STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.')
+ end
end
diff --git a/lib/generators/datatable/config_generator.rb b/lib/generators/datatable/config_generator.rb
deleted file mode 100644
index ea5ff476..00000000
--- a/lib/generators/datatable/config_generator.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-require 'rails/generators'
-
-module Datatable
- module Generators
- class ConfigGenerator < ::Rails::Generators::Base
- source_root File.expand_path(File.join(File.dirname(__FILE__), 'templates'))
- desc < < AjaxDatatablesRails::Base
+class <%= datatable_name %> < AjaxDatatablesRails::ActiveRecord
def view_columns
# Declare strings in this format: ModelName.column_name
@@ -19,23 +19,9 @@ def data
end
end
- private
-
def get_raw_records
# insert query here
+ # User.all
end
- # ==== These methods represent the basic operations to perform on records
- # and feel free to override them
-
- # def filter_records(records)
- # end
-
- # def sort_records(records)
- # end
-
- # def paginate_records(records)
- # end
-
- # ==== Insert 'presenter'-like methods below if necessary
end
diff --git a/spec/ajax-datatables-rails/base_spec.rb b/spec/ajax-datatables-rails/base_spec.rb
deleted file mode 100644
index 39e5a738..00000000
--- a/spec/ajax-datatables-rails/base_spec.rb
+++ /dev/null
@@ -1,187 +0,0 @@
-require 'spec_helper'
-
-describe AjaxDatatablesRails::Base do
-
- describe 'an instance' do
- let(:view) { double('view', params: sample_params) }
-
- it 'requires a view_context' do
- expect { described_class.new }.to raise_error ArgumentError
- end
-
- it 'accepts an options hash' do
- datatable = described_class.new(view, foo: 'bar')
- expect(datatable.options).to eq(foo: 'bar')
- end
- end
-
- context 'Public API' do
- let(:view) { double('view', params: sample_params) }
-
- describe '#view_columns' do
- it 'raises an error if not defined by the user' do
- datatable = described_class.new(view)
- expect { datatable.view_columns }.to raise_error NotImplementedError
- end
-
- context 'child class implements view_columns' do
- it 'expects a hash based defining columns' do
- datatable = ComplexDatatable.new(view)
- expect(datatable.view_columns).to be_a(Hash)
- end
- end
- end
-
- describe '#get_raw_records' do
- it 'raises an error if not defined by the user' do
- datatable = described_class.new(view)
- expect { datatable.get_raw_records }.to raise_error NotImplementedError
- end
- end
-
- describe '#data' do
- it 'raises an error if not defined by the user' do
- datatable = described_class.new(view)
- expect { datatable.data }.to raise_error NotImplementedError
- end
-
- context 'when data is defined as a hash' do
- let(:datatable) { ComplexDatatable.new(view) }
-
- it 'should return an array of hashes' do
- create_list(:user, 5)
- expect(datatable.data).to be_a(Array)
- expect(datatable.data.size).to eq 5
- item = datatable.data.first
- expect(item).to be_a(Hash)
- end
-
- it 'should html escape data' do
- create(:user, first_name: 'Name "> ', last_name: 'Name "> ')
- data = datatable.send(:sanitize, datatable.data)
- item = data.first
- expect(item[:first_name]).to eq 'Name "><img src=x onerror=alert("first_name")>'
- expect(item[:last_name]).to eq 'Name "><img src=x onerror=alert("last_name")>'
- end
- end
-
- context 'when data is defined as a array' do
- let(:datatable) { ComplexDatatableArray.new(view) }
-
- it 'should return an array of arrays' do
- create_list(:user, 5)
- expect(datatable.data).to be_a(Array)
- expect(datatable.data.size).to eq 5
- item = datatable.data.first
- expect(item).to be_a(Array)
- end
-
- it 'should html escape data' do
- create(:user, first_name: 'Name "> ', last_name: 'Name "> ')
- data = datatable.send(:sanitize, datatable.data)
- item = data.first
- expect(item[2]).to eq 'Name "><img src=x onerror=alert("first_name")>'
- expect(item[3]).to eq 'Name "><img src=x onerror=alert("last_name")>'
- end
- end
- end
-
- describe '#as_json' do
- let(:datatable) { ComplexDatatable.new(view) }
-
- it 'should return a hash' do
- create_list(:user, 5)
- data = datatable.as_json
- expect(data[:recordsTotal]).to eq 5
- expect(data[:recordsFiltered]).to eq 5
- expect(data[:data]).to be_a(Array)
- expect(data[:data].size).to eq 5
- end
-
- context 'with additional_datas' do
- it 'should return a hash' do
- create_list(:user, 5)
- expect(datatable).to receive(:additional_datas){ { foo: 'bar' } }
- data = datatable.as_json
- expect(data[:recordsTotal]).to eq 5
- expect(data[:recordsFiltered]).to eq 5
- expect(data[:data]).to be_a(Array)
- expect(data[:data].size).to eq 5
- expect(data[:foo]).to eq 'bar'
- end
- end
- end
- end
-
-
- context 'Private API' do
-
- let(:view) { double('view', params: sample_params) }
- let(:datatable) { ComplexDatatable.new(view) }
-
- before(:each) do
- allow_any_instance_of(AjaxDatatablesRails::Configuration).to receive(:orm) { nil }
- end
-
- describe '#fetch_records' do
- it 'raises an error if it does not include an ORM module' do
- expect { datatable.send(:fetch_records) }.to raise_error NoMethodError
- end
- end
-
- describe '#filter_records' do
- it 'raises an error if it does not include an ORM module' do
- expect { datatable.send(:filter_records) }.to raise_error NoMethodError
- end
- end
-
- describe '#sort_records' do
- it 'raises an error if it does not include an ORM module' do
- expect { datatable.send(:sort_records) }.to raise_error NoMethodError
- end
- end
-
- describe '#paginate_records' do
- it 'raises an error if it does not include an ORM module' do
- expect { datatable.send(:paginate_records) }.to raise_error NoMethodError
- end
- end
-
- describe 'helper methods' do
- describe '#offset' do
- it 'defaults to 0' do
- default_view = double('view', params: {})
- datatable = described_class.new(default_view)
- expect(datatable.datatable.send(:offset)).to eq(0)
- end
-
- it 'matches the value on view params[:start] minus 1' do
- paginated_view = double('view', params: { start: '11' })
- datatable = described_class.new(paginated_view)
- expect(datatable.datatable.send(:offset)).to eq(10)
- end
- end
-
- describe '#page' do
- it 'calculates page number from params[:start] and #per_page' do
- paginated_view = double('view', params: { start: '11' })
- datatable = described_class.new(paginated_view)
- expect(datatable.datatable.send(:page)).to eq(2)
- end
- end
-
- describe '#per_page' do
- it 'defaults to 10' do
- datatable = described_class.new(view)
- expect(datatable.datatable.send(:per_page)).to eq(10)
- end
-
- it 'matches the value on view params[:length]' do
- other_view = double('view', params: { length: 20 })
- datatable = described_class.new(other_view)
- expect(datatable.datatable.send(:per_page)).to eq(20)
- end
- end
- end
- end
-end
diff --git a/spec/ajax-datatables-rails/configuration_spec.rb b/spec/ajax-datatables-rails/configuration_spec.rb
deleted file mode 100644
index 0e11d68b..00000000
--- a/spec/ajax-datatables-rails/configuration_spec.rb
+++ /dev/null
@@ -1,43 +0,0 @@
-require 'spec_helper'
-
-describe AjaxDatatablesRails do
- describe 'configurations' do
- context 'configurable from outside' do
- before(:each) do
- AjaxDatatablesRails.configure do |config|
- config.db_adapter = :mysql
- end
- end
-
- it 'should have custom value' do
- expect(AjaxDatatablesRails.config.db_adapter).to eq(:mysql)
- end
- end
- end
-end
-
-describe AjaxDatatablesRails::Configuration do
- let(:config) { AjaxDatatablesRails::Configuration.new }
-
- describe 'default config' do
- it 'default orm should :active_record' do
- expect(config.orm).to eq(:active_record)
- end
-
- it 'default db_adapter should :postgresql' do
- expect(config.db_adapter).to eq(:postgresql)
- end
- end
-
- describe 'custom config' do
- it 'should accept db_adapter custom value' do
- config.db_adapter = :mysql
- expect(config.db_adapter).to eq(:mysql)
- end
-
- it 'accepts a custom orm value' do
- config.orm = :mongoid
- expect(config.orm).to eq(:mongoid)
- end
- end
-end
diff --git a/spec/ajax-datatables-rails/datatable/column_spec.rb b/spec/ajax-datatables-rails/datatable/column_spec.rb
deleted file mode 100644
index bf1daf53..00000000
--- a/spec/ajax-datatables-rails/datatable/column_spec.rb
+++ /dev/null
@@ -1,104 +0,0 @@
-require 'spec_helper'
-
-describe AjaxDatatablesRails::Datatable::Column do
-
- let(:view) { double('view', params: sample_params) }
- let(:datatable) { ComplexDatatable.new(view) }
-
- describe 'username column' do
-
- let(:column) { datatable.datatable.columns.first }
-
- before do
- datatable.params[:columns] = {'0'=>{'data'=>'username', 'name'=>'', 'searchable'=>'true', 'orderable'=>'true', 'search'=>{'value'=>'searchvalue', 'regex'=>'false'}}}
- end
-
- it 'should be orderable' do
- expect(column.orderable?).to eq(true)
- end
-
- it 'should be searchable' do
- expect(column.searchable?).to eq(true)
- end
-
- it 'should be searched' do
- expect(column.searched?).to eq(true)
- end
-
- it 'should have connected to id column' do
- expect(column.data).to eq('username')
- end
-
- describe '#search' do
- it 'child class' do
- expect(column.search).to be_a(AjaxDatatablesRails::Datatable::SimpleSearch)
- end
-
- it 'should have search value' do
- expect(column.search.value).to eq('searchvalue')
- end
-
- it 'should not regex' do
- expect(column.search.regexp?).to eq false
- end
- end
-
- describe '#cond' do
- it 'should be :like by default' do
- expect(column.cond).to eq(:like)
- end
- end
-
- describe '#source' do
- it 'should be :like by default' do
- expect(column.source).to eq('User.username')
- end
- end
-
- describe '#search_query' do
- it 'should buld search query' do
- expect(column.search_query.to_sql).to include('%searchvalue%')
- end
- end
-
- describe '#sort_query' do
- it 'should build sort query' do
- expect(column.sort_query).to eq('users.username')
- end
- end
-
- describe '#use_regex?' do
- it 'should be true by default' do
- expect(column.use_regex?).to be true
- end
- end
-
- describe '#delimiter' do
- it 'should be - by default' do
- expect(column.delimiter).to eq('-')
- end
- end
- end
-
- describe '#formater' do
- let(:datatable) { DatatableWithFormater.new(view) }
- let(:column) { datatable.datatable.columns.find { |c| c.data == 'last_name' } }
-
- it 'should be a proc' do
- expect(column.formater).to be_a(Proc)
- end
- end
-
- describe '#filter' do
- let(:datatable) { DatatableCondProc.new(view) }
- let(:column) { datatable.datatable.columns.find { |c| c.data == 'username' } }
-
- it 'should be a proc' do
- config = column.instance_variable_get('@view_column')
- filter = config[:cond]
- expect(filter).to be_a(Proc)
- expect(filter).to receive(:call).with(column, column.formated_value)
- column.filter
- end
- end
-end
diff --git a/spec/ajax-datatables-rails/datatable/datatable_spec.rb b/spec/ajax-datatables-rails/datatable/datatable_spec.rb
deleted file mode 100644
index b7c1d3d2..00000000
--- a/spec/ajax-datatables-rails/datatable/datatable_spec.rb
+++ /dev/null
@@ -1,87 +0,0 @@
-require 'spec_helper'
-
-describe AjaxDatatablesRails::Datatable::Datatable do
-
- let(:view) { double('view', params: sample_params) }
- let(:datatable) { ComplexDatatable.new(view).datatable }
- let(:order_option) { {'0'=>{'column'=>'0', 'dir'=>'asc'}, '1'=>{'column'=>'1', 'dir'=>'desc'}} }
-
- describe 'order methods' do
- it 'should be orderable' do
- expect(datatable.orderable?).to eq(true)
- end
-
- it 'should not be orderable' do
- datatable.options[:order] = nil
- expect(datatable.orderable?).to eq(false)
- end
-
- it 'should have 2 orderable columns' do
- datatable.options[:order] = order_option
- expect(datatable.orders.count).to eq(2)
- end
-
- it 'first column ordered by ASC' do
- datatable.options[:order] = order_option
- expect(datatable.orders.first.direction).to eq('ASC')
- end
-
- it 'first column ordered by DESC' do
- datatable.options[:order] = order_option
- expect(datatable.orders.last.direction).to eq('DESC')
- end
-
- it 'child class' do
- expect(datatable.orders.first).to be_a(AjaxDatatablesRails::Datatable::SimpleOrder)
- end
- end
-
- describe 'search methods' do
- it 'should be searchable' do
- datatable.options[:search][:value] = 'atom'
- expect(datatable.searchable?).to eq(true)
- end
-
- it 'should not be searchable' do
- datatable.options[:search][:value] = nil
- expect(datatable.searchable?).to eq(false)
- end
-
- it 'child class' do
- expect(datatable.search).to be_a(AjaxDatatablesRails::Datatable::SimpleSearch)
- end
- end
-
- describe 'columns methods' do
- it 'should have 4 columns' do
- expect(datatable.columns.count).to eq(6)
- end
-
- it 'child class' do
- expect(datatable.columns.first).to be_a(AjaxDatatablesRails::Datatable::Column)
- end
- end
-
- describe 'option methods' do
- before :each do
- datatable.options[:start] = '50'
- datatable.options[:length] = '15'
- end
-
- it 'paginate?' do
- expect(datatable.paginate?).to be(true)
- end
-
- it 'offset' do
- expect(datatable.offset).to eq(45)
- end
-
- it 'page' do
- expect(datatable.page).to eq(4)
- end
-
- it 'per_page' do
- expect(datatable.per_page).to eq(15)
- end
- end
-end
diff --git a/spec/ajax-datatables-rails/datatable/simple_order_spec.rb b/spec/ajax-datatables-rails/datatable/simple_order_spec.rb
deleted file mode 100644
index 296fd044..00000000
--- a/spec/ajax-datatables-rails/datatable/simple_order_spec.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-require 'spec_helper'
-
-describe AjaxDatatablesRails::Datatable::SimpleOrder do
-
- let(:options) { ActiveSupport::HashWithIndifferentAccess.new({'column'=>'1', 'dir'=>'desc'}) }
- let(:simple_order) { AjaxDatatablesRails::Datatable::SimpleOrder.new(nil, options) }
-
- describe 'option methods' do
- it 'sql query' do
- expect(simple_order.query('firstname')).to eq('firstname DESC')
- end
- end
-end
diff --git a/spec/ajax-datatables-rails/extended_spec.rb b/spec/ajax-datatables-rails/extended_spec.rb
deleted file mode 100644
index de023fc1..00000000
--- a/spec/ajax-datatables-rails/extended_spec.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-require 'spec_helper'
-
-describe AjaxDatatablesRails::Base do
- describe 'it can transform search value before asking the database' do
- let(:view) { double('view', params: sample_params) }
- let(:datatable) { DatatableWithFormater.new(view) }
-
- before(:each) do
- create(:user, username: 'johndoe', email: 'johndoe@example.com', last_name: 'DOE')
- create(:user, username: 'msmith', email: 'mary.smith@example.com', last_name: 'SMITH')
- datatable.params[:columns]['3'][:search][:value] = 'doe'
- end
-
- it 'should filter records' do
- expect(datatable.data.size).to eq 1
- item = datatable.data.first
- expect(item[:last_name]).to eq 'DOE'
- end
- end
-end
diff --git a/spec/ajax-datatables-rails/orm/active_record_filter_records_spec.rb b/spec/ajax-datatables-rails/orm/active_record_filter_records_spec.rb
deleted file mode 100644
index bcf66d67..00000000
--- a/spec/ajax-datatables-rails/orm/active_record_filter_records_spec.rb
+++ /dev/null
@@ -1,487 +0,0 @@
-require 'spec_helper'
-
-describe AjaxDatatablesRails::ORM::ActiveRecord do
-
- let(:view) { double('view', params: sample_params) }
- let(:datatable) { ComplexDatatable.new(view) }
- let(:records) { User.all }
-
- describe '#filter_records' do
- it 'requires a records collection as argument' do
- expect { datatable.send(:filter_records) }.to raise_error(ArgumentError)
- end
-
- it 'performs a simple search first' do
- datatable.params[:search] = { value: 'msmith' }
- expect(datatable).to receive(:build_conditions_for_datatable)
- datatable.send(:filter_records, records)
- end
-
- it 'performs a composite search second' do
- datatable.params[:search] = { value: '' }
- expect(datatable).to receive(:build_conditions_for_selected_columns)
- datatable.send(:filter_records, records)
- end
- end
-
- describe '#build_conditions_for_datatable' do
- before(:each) do
- create(:user, username: 'johndoe', email: 'johndoe@example.com')
- create(:user, username: 'msmith', email: 'mary.smith@example.com')
- end
-
- it 'returns an Arel object' do
- datatable.params[:search] = { value: 'msmith' }
- result = datatable.send(:build_conditions_for_datatable)
- expect(result).to be_a(Arel::Nodes::Grouping)
- end
-
- context 'no search query' do
- it 'returns empty query' do
- datatable.params[:search] = { value: '' }
- expect(datatable.send(:build_conditions_for_datatable)).to be_blank
- end
- end
-
- context 'when none of columns are connected' do
- before(:each) do
- allow(datatable).to receive(:searchable_columns) { [] }
- end
-
- context 'when search value is a string' do
- before(:each) do
- datatable.params[:search] = { value: 'msmith' }
- end
-
- it 'returns empty query result' do
- expect(datatable.send(:build_conditions_for_datatable)).to be_blank
- end
-
- it 'returns filtered results' do
- query = datatable.send(:build_conditions_for_datatable)
- results = records.where(query).map(&:username)
- expect(results).to eq ['johndoe', 'msmith']
- end
- end
-
- context 'when search value is space-separated string' do
- before(:each) do
- datatable.params[:search] = { value: 'foo bar' }
- end
-
- it 'returns empty query result' do
- expect(datatable.send(:build_conditions_for_datatable)).to be_blank
- end
-
- it 'returns filtered results' do
- query = datatable.send(:build_conditions_for_datatable)
- results = records.where(query).map(&:username)
- expect(results).to eq ['johndoe', 'msmith']
- end
- end
- end
-
- context 'with search query' do
- context 'when search value is a string' do
- before(:each) do
- datatable.params[:search] = { value: 'john', regex: 'false' }
- end
-
- it 'returns a filtering query' do
- query = datatable.send(:build_conditions_for_datatable)
- results = records.where(query).map(&:username)
- expect(results).to include('johndoe')
- expect(results).not_to include('msmith')
- end
- end
-
- context 'when search value is space-separated string' do
- before(:each) do
- datatable.params[:search] = { value: 'john doe', regex: 'false' }
- end
-
- it 'returns a filtering query' do
- query = datatable.send(:build_conditions_for_datatable)
- results = records.where(query).map(&:username)
- expect(results).to eq ['johndoe']
- expect(results).not_to include('msmith')
- end
- end
- end
- end
-
- describe '#build_conditions_for_selected_columns' do
- before(:each) do
- create(:user, username: 'johndoe', email: 'johndoe@example.com')
- create(:user, username: 'msmith', email: 'mary.smith@example.com')
- end
-
- context 'columns include search query' do
- before do
- datatable.params[:columns]['0'][:search][:value] = 'doe'
- datatable.params[:columns]['1'][:search][:value] = 'example'
- end
-
- it 'returns an Arel object' do
- result = datatable.send(:build_conditions_for_selected_columns)
- expect(result).to be_a(Arel::Nodes::And)
- end
-
- if AjaxDatatablesRails.config.db_adapter == :postgresql
- context 'when db_adapter is postgresql' do
- it 'can call #to_sql on returned object' do
- result = datatable.send(:build_conditions_for_selected_columns)
- expect(result).to respond_to(:to_sql)
- expect(result.to_sql).to eq(
- "CAST(\"users\".\"username\" AS VARCHAR) ILIKE '%doe%' AND CAST(\"users\".\"email\" AS VARCHAR) ILIKE '%example%'"
- )
- end
- end
- end
-
- if AjaxDatatablesRails.config.db_adapter.in? %i[oracle oracleenhanced]
- context 'when db_adapter is oracle' do
- it 'can call #to_sql on returned object' do
- result = datatable.send(:build_conditions_for_selected_columns)
- expect(result).to respond_to(:to_sql)
- expect(result.to_sql).to eq(
- "CAST(\"USERS\".\"USERNAME\" AS VARCHAR2(4000)) LIKE '%doe%' AND CAST(\"USERS\".\"EMAIL\" AS VARCHAR2(4000)) LIKE '%example%'"
- )
- end
- end
- end
-
- if AjaxDatatablesRails.config.db_adapter.in? %i[mysql2 sqlite3]
- context 'when db_adapter is mysql2' do
- it 'can call #to_sql on returned object' do
- result = datatable.send(:build_conditions_for_selected_columns)
- expect(result).to respond_to(:to_sql)
- expect(result.to_sql).to eq(
- "CAST(`users`.`username` AS CHAR) LIKE '%doe%' AND CAST(`users`.`email` AS CHAR) LIKE '%example%'"
- )
- end
- end
- end
- end
-
- it 'calls #build_conditions_for_selected_columns' do
- expect(datatable).to receive(:build_conditions_for_selected_columns)
- datatable.send(:build_conditions)
- end
-
- context 'with search values in columns' do
- before(:each) do
- datatable.params[:columns]['0'][:search][:value] = 'doe'
- end
-
- it 'returns a filtered set of records' do
- query = datatable.send(:build_conditions_for_selected_columns)
- results = records.where(query).map(&:username)
- expect(results).to include('johndoe')
- expect(results).not_to include('msmith')
- end
- end
- end
-
- describe '#typecast helper method' do
- let(:view) { double('view', params: sample_params) }
- let(:column) { ComplexDatatable.new(view).datatable.columns.first }
-
- it 'returns VARCHAR if :db_adapter is :pg' do
- allow_any_instance_of(AjaxDatatablesRails::Configuration).to receive(:db_adapter) { :pg }
- expect(column.send(:typecast)).to eq('VARCHAR')
- end
-
- it 'returns VARCHAR if :db_adapter is :postgre' do
- allow_any_instance_of(AjaxDatatablesRails::Configuration).to receive(:db_adapter) { :postgre }
- expect(column.send(:typecast)).to eq('VARCHAR')
- end
-
- it 'returns VARCHAR if :db_adapter is :postgresql' do
- allow_any_instance_of(AjaxDatatablesRails::Configuration).to receive(:db_adapter) { :postgresql }
- expect(column.send(:typecast)).to eq('VARCHAR')
- end
-
- it 'returns VARCHAR if :db_adapter is :oracle' do
- allow_any_instance_of(AjaxDatatablesRails::Configuration).to receive(:db_adapter) { :oracle }
- expect(column.send(:typecast)).to eq('VARCHAR2(4000)')
- end
-
- it 'returns VARCHAR if :db_adapter is :oracleenhanced' do
- allow_any_instance_of(AjaxDatatablesRails::Configuration).to receive(:db_adapter) { :oracleenhanced }
- expect(column.send(:typecast)).to eq('VARCHAR2(4000)')
- end
-
- it 'returns CHAR if :db_adapter is :mysql2' do
- allow_any_instance_of(AjaxDatatablesRails::Configuration).to receive(:db_adapter) { :mysql2 }
- expect(column.send(:typecast)).to eq('CHAR')
- end
-
- it 'returns CHAR if :db_adapter is :mysql' do
- allow_any_instance_of(AjaxDatatablesRails::Configuration).to receive(:db_adapter) { :mysql }
- expect(column.send(:typecast)).to eq('CHAR')
- end
-
- it 'returns TEXT if :db_adapter is :sqlite' do
- allow_any_instance_of(AjaxDatatablesRails::Configuration).to receive(:db_adapter) { :sqlite }
- expect(column.send(:typecast)).to eq('TEXT')
- end
-
- it 'returns TEXT if :db_adapter is :sqlite3' do
- allow_any_instance_of(AjaxDatatablesRails::Configuration).to receive(:db_adapter) { :sqlite3 }
- expect(column.send(:typecast)).to eq('TEXT')
- end
- end
-
- describe 'filter conditions' do
- unless AjaxDatatablesRails.old_rails?
- describe 'it can filter records with condition :date_range' do
- let(:datatable) { DatatableCondDate.new(view) }
-
- before(:each) do
- create(:user, username: 'johndoe', email: 'johndoe@example.com', last_name: 'Doe', created_at: '01/01/2000')
- create(:user, username: 'msmith', email: 'mary.smith@example.com', last_name: 'Smith', created_at: '01/02/2000')
- end
-
- context 'when range is empty' do
- it 'should not filter records' do
- datatable.params[:columns]['5'][:search][:value] = '-'
- expect(datatable.data.size).to eq 2
- item = datatable.data.first
- expect(item[:last_name]).to eq 'Doe'
- end
- end
-
- context 'when start date is filled' do
- it 'should filter records created after this date' do
- datatable.params[:columns]['5'][:search][:value] = '31/12/1999-'
- expect(datatable.data.size).to eq 2
- end
- end
-
- context 'when end date is filled' do
- it 'should filter records created before this date' do
- datatable.params[:columns]['5'][:search][:value] = '-31/12/1999'
- expect(datatable.data.size).to eq 0
- end
- end
-
- context 'when both date are filled' do
- it 'should filter records created between the range' do
- datatable.params[:columns]['5'][:search][:value] = '01/12/1999-15/01/2000'
- expect(datatable.data.size).to eq 1
- end
- end
-
- context 'when another filter is active' do
- context 'when range is empty' do
- it 'should filter records' do
- datatable.params[:columns]['0'][:search][:value] = 'doe'
- datatable.params[:columns]['5'][:search][:value] = '-'
- expect(datatable.data.size).to eq 1
- item = datatable.data.first
- expect(item[:last_name]).to eq 'Doe'
- end
- end
-
- context 'when start date is filled' do
- it 'should filter records' do
- datatable.params[:columns]['0'][:search][:value] = 'doe'
- datatable.params[:columns]['5'][:search][:value] = '01/12/1999-'
- expect(datatable.data.size).to eq 1
- item = datatable.data.first
- expect(item[:last_name]).to eq 'Doe'
- end
- end
-
- context 'when end date is filled' do
- it 'should filter records' do
- datatable.params[:columns]['0'][:search][:value] = 'doe'
- datatable.params[:columns]['5'][:search][:value] = '-15/01/2000'
- expect(datatable.data.size).to eq 1
- item = datatable.data.first
- expect(item[:last_name]).to eq 'Doe'
- end
- end
-
- context 'when both date are filled' do
- it 'should filter records' do
- datatable.params[:columns]['0'][:search][:value] = 'doe'
- datatable.params[:columns]['5'][:search][:value] = '01/12/1999-15/01/2000'
- expect(datatable.data.size).to eq 1
- item = datatable.data.first
- expect(item[:last_name]).to eq 'Doe'
- end
- end
- end
- end
- end
-
- describe 'it can filter records with condition :start_with' do
- let(:datatable) { DatatableCondStartWith.new(view) }
-
- before(:each) do
- create(:user, first_name: 'John')
- create(:user, first_name: 'Mary')
- end
-
- it 'should filter records matching' do
- datatable.params[:columns]['2'][:search][:value] = 'Jo'
- expect(datatable.data.size).to eq 1
- item = datatable.data.first
- expect(item[:first_name]).to eq 'John'
- end
- end
-
- describe 'it can filter records with condition :end_with' do
- let(:datatable) { DatatableCondEndWith.new(view) }
-
- before(:each) do
- create(:user, last_name: 'JOHN')
- create(:user, last_name: 'MARY')
- end
-
- it 'should filter records matching' do
- datatable.params[:columns]['3'][:search][:value] = 'ry'
- expect(datatable.data.size).to eq 1
- item = datatable.data.first
- expect(item[:last_name]).to eq 'MARY'
- end
- end
-
- describe 'it can filter records with condition :null_value' do
- let(:datatable) { DatatableCondNullValue.new(view) }
-
- before(:each) do
- create(:user, first_name: 'john', email: 'foo@bar.com')
- create(:user, first_name: 'mary', email: nil)
- end
-
- context 'when condition is NULL' do
- it 'should filter records matching' do
- datatable.params[:columns]['1'][:search][:value] = 'NULL'
- expect(datatable.data.size).to eq 1
- item = datatable.data.first
- expect(item[:first_name]).to eq 'mary'
- end
- end
-
- context 'when condition is !NULL' do
- it 'should filter records matching' do
- datatable.params[:columns]['1'][:search][:value] = '!NULL'
- expect(datatable.data.size).to eq 1
- item = datatable.data.first
- expect(item[:first_name]).to eq 'john'
- end
- end
- end
-
- describe 'it can filter records with condition :eq' do
- let(:datatable) { DatatableCondEq.new(view) }
-
- before(:each) do
- create(:user, first_name: 'john', post_id: 1)
- create(:user, first_name: 'mary', post_id: 2)
- end
-
- it 'should filter records matching' do
- datatable.params[:columns]['4'][:search][:value] = 1
- expect(datatable.data.size).to eq 1
- item = datatable.data.first
- expect(item[:first_name]).to eq 'john'
- end
- end
-
- describe 'it can filter records with condition :not_eq' do
- let(:datatable) { DatatableCondNotEq.new(view) }
-
- before(:each) do
- create(:user, first_name: 'john', post_id: 1)
- create(:user, first_name: 'mary', post_id: 2)
- end
-
- it 'should filter records matching' do
- datatable.params[:columns]['4'][:search][:value] = 1
- expect(datatable.data.size).to eq 1
- item = datatable.data.first
- expect(item[:first_name]).to eq 'mary'
- end
- end
-
- describe 'it can filter records with condition :lt' do
- let(:datatable) { DatatableCondLt.new(view) }
-
- before(:each) do
- create(:user, first_name: 'john', post_id: 1)
- create(:user, first_name: 'mary', post_id: 2)
- end
-
- it 'should filter records matching' do
- datatable.params[:columns]['4'][:search][:value] = 2
- expect(datatable.data.size).to eq 1
- item = datatable.data.first
- expect(item[:first_name]).to eq 'john'
- end
- end
-
- describe 'it can filter records with condition :gt' do
- let(:datatable) { DatatableCondGt.new(view) }
-
- before(:each) do
- create(:user, first_name: 'john', post_id: 1)
- create(:user, first_name: 'mary', post_id: 2)
- end
-
- it 'should filter records matching' do
- datatable.params[:columns]['4'][:search][:value] = 1
- expect(datatable.data.size).to eq 1
- item = datatable.data.first
- expect(item[:first_name]).to eq 'mary'
- end
- end
-
- describe 'it can filter records with condition :lteq' do
- let(:datatable) { DatatableCondLteq.new(view) }
-
- before(:each) do
- create(:user, first_name: 'john', post_id: 1)
- create(:user, first_name: 'mary', post_id: 2)
- end
-
- it 'should filter records matching' do
- datatable.params[:columns]['4'][:search][:value] = 2
- expect(datatable.data.size).to eq 2
- end
- end
-
- describe 'it can filter records with condition :gteq' do
- let(:datatable) { DatatableCondGteq.new(view) }
-
- before(:each) do
- create(:user, first_name: 'john', post_id: 1)
- create(:user, first_name: 'mary', post_id: 2)
- end
-
- it 'should filter records matching' do
- datatable.params[:columns]['4'][:search][:value] = 1
- expect(datatable.data.size).to eq 2
- end
- end
-
- describe 'it can filter records with condition :in' do
- let(:datatable) { DatatableCondIn.new(view) }
-
- before(:each) do
- create(:user, first_name: 'john', post_id: 1)
- create(:user, first_name: 'mary', post_id: 2)
- end
-
- it 'should filter records matching' do
- datatable.params[:columns]['4'][:search][:value] = [1]
- expect(datatable.data.size).to eq 1
- item = datatable.data.first
- expect(item[:first_name]).to eq 'john'
- end
- end
- end
-end
diff --git a/spec/ajax-datatables-rails/orm/active_record_sort_records_spec.rb b/spec/ajax-datatables-rails/orm/active_record_sort_records_spec.rb
deleted file mode 100644
index ccd19511..00000000
--- a/spec/ajax-datatables-rails/orm/active_record_sort_records_spec.rb
+++ /dev/null
@@ -1,34 +0,0 @@
-require 'spec_helper'
-
-describe AjaxDatatablesRails::ORM::ActiveRecord do
-
- let(:view) { double('view', params: sample_params) }
- let(:datatable) { ComplexDatatable.new(view) }
- let(:records) { User.all }
-
- before(:each) do
- create(:user, username: 'johndoe', email: 'johndoe@example.com')
- create(:user, username: 'msmith', email: 'mary.smith@example.com')
- end
-
- describe '#sort_records' do
- it 'returns a records collection sorted by :order params' do
- # set to order Users by email in descending order
- datatable.params[:order]['0'] = { column: '1', dir: 'desc' }
- expect(datatable.sort_records(records).map(&:email)).to match(
- ['mary.smith@example.com', 'johndoe@example.com']
- )
- end
-
- it 'can handle multiple sorting columns' do
- # set to order by Users username in ascending order, and
- # by Users email in descending order
- datatable.params[:order]['0'] = { column: '0', dir: 'asc' }
- datatable.params[:order]['1'] = { column: '1', dir: 'desc' }
- expect(datatable.sort_records(records).to_sql).to include(
- 'ORDER BY users.username ASC, users.email DESC'
- )
- end
- end
-
-end
diff --git a/spec/ajax-datatables-rails/orm/active_record_spec.rb b/spec/ajax-datatables-rails/orm/active_record_spec.rb
deleted file mode 100644
index 332a10ce..00000000
--- a/spec/ajax-datatables-rails/orm/active_record_spec.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-require 'spec_helper'
-
-describe AjaxDatatablesRails::ORM::ActiveRecord do
- context 'Private API' do
- let(:view) { double('view', params: sample_params) }
- let(:datatable) { ComplexDatatable.new(view) }
-
- before(:each) do
- create(:user, username: 'johndoe', email: 'johndoe@example.com')
- create(:user, username: 'msmith', email: 'mary.smith@example.com')
- end
-
- describe '#fetch_records' do
- it 'calls #get_raw_records' do
- expect(datatable).to receive(:get_raw_records) { User.all }
- datatable.send(:fetch_records)
- end
-
- it 'returns a collection of records' do
- expect(datatable).to receive(:get_raw_records) { User.all }
- expect(datatable.send(:fetch_records)).to be_a(ActiveRecord::Relation)
- end
- end
- end
-end
diff --git a/spec/ajax_datatables_rails/base_spec.rb b/spec/ajax_datatables_rails/base_spec.rb
new file mode 100644
index 00000000..28915792
--- /dev/null
+++ b/spec/ajax_datatables_rails/base_spec.rb
@@ -0,0 +1,242 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe AjaxDatatablesRails::Base do
+
+ describe 'an instance' do
+ it 'requires a hash of params' do
+ expect { described_class.new }.to raise_error ArgumentError
+ end
+
+ it 'accepts an options hash' do
+ datatable = described_class.new(sample_params, foo: 'bar')
+ expect(datatable.options).to eq(foo: 'bar')
+ end
+ end
+
+ describe 'User API' do
+ describe '#view_columns' do
+ context 'when method is not defined by the user' do
+ it 'raises an error' do
+ datatable = described_class.new(sample_params)
+ expect { datatable.view_columns }.to raise_error(NotImplementedError).with_message(<<~ERROR)
+
+ You should implement this method in your class and return an array
+ of database columns based on the columns displayed in the HTML view.
+ These columns should be represented in the ModelName.column_name,
+ or aliased_join_table.column_name notation.
+ ERROR
+ end
+ end
+
+ context 'when child class implements view_columns' do
+ it 'expects a hash based defining columns' do
+ datatable = ComplexDatatable.new(sample_params)
+ expect(datatable.view_columns).to be_a(Hash)
+ end
+ end
+ end
+
+ describe '#get_raw_records' do
+ context 'when method is not defined by the user' do
+ it 'raises an error' do
+ datatable = described_class.new(sample_params)
+ expect { datatable.get_raw_records }.to raise_error(NotImplementedError).with_message(<<~ERROR)
+
+ You should implement this method in your class and specify
+ how records are going to be retrieved from the database.
+ ERROR
+ end
+ end
+ end
+
+ describe '#data' do
+ context 'when method is not defined by the user' do
+ it 'raises an error' do
+ datatable = described_class.new(sample_params)
+ expect { datatable.data }.to raise_error(NotImplementedError).with_message(<<~ERROR)
+
+ You should implement this method in your class and return an array
+ of arrays, or an array of hashes, as defined in the jQuery.dataTables
+ plugin documentation.
+ ERROR
+ end
+ end
+
+ context 'when data is defined as a hash' do
+ let(:datatable) { ComplexDatatable.new(sample_params) }
+
+ it 'returns an array of hashes' do
+ create_list(:user, 5)
+ expect(datatable.data).to be_a(Array)
+ expect(datatable.data.size).to eq 5
+ item = datatable.data.first
+ expect(item).to be_a(Hash)
+ end
+
+ it 'htmls escape data' do
+ create(:user, first_name: 'Name "> ', last_name: 'Name "> ')
+ data = datatable.send(:sanitize_data, datatable.data)
+ item = data.first
+ expect(item[:first_name]).to eq 'Name "><img src=x onerror=alert("first_name")>'
+ expect(item[:last_name]).to eq 'Name "><img src=x onerror=alert("last_name")>'
+ end
+ end
+
+ context 'when data is defined as a array' do
+ let(:datatable) { ComplexDatatableArray.new(sample_params) }
+
+ it 'returns an array of arrays' do
+ create_list(:user, 5)
+ expect(datatable.data).to be_a(Array)
+ expect(datatable.data.size).to eq 5
+ item = datatable.data.first
+ expect(item).to be_a(Array)
+ end
+
+ it 'htmls escape data' do
+ create(:user, first_name: 'Name "> ', last_name: 'Name "> ')
+ data = datatable.send(:sanitize_data, datatable.data)
+ item = data.first
+ expect(item[2]).to eq 'Name "><img src=x onerror=alert("first_name")>'
+ expect(item[3]).to eq 'Name "><img src=x onerror=alert("last_name")>'
+ end
+ end
+ end
+ end
+
+ describe 'ORM API' do
+ context 'when ORM is not implemented' do
+ let(:datatable) { described_class.new(sample_params) }
+
+ describe '#fetch_records' do
+ it 'raises an error if it does not include an ORM module' do
+ expect { datatable.fetch_records }.to raise_error NotImplementedError
+ end
+ end
+
+ describe '#filter_records' do
+ it 'raises an error if it does not include an ORM module' do
+ expect { datatable.filter_records([]) }.to raise_error NotImplementedError
+ end
+ end
+
+ describe '#sort_records' do
+ it 'raises an error if it does not include an ORM module' do
+ expect { datatable.sort_records([]) }.to raise_error NotImplementedError
+ end
+ end
+
+ describe '#paginate_records' do
+ it 'raises an error if it does not include an ORM module' do
+ expect { datatable.paginate_records([]) }.to raise_error NotImplementedError
+ end
+ end
+ end
+
+ context 'when ORM is implemented' do
+ describe 'it allows method override' do
+ let(:datatable) do
+ datatable = Class.new(ComplexDatatable) do
+ def filter_records(_records)
+ raise NotImplementedError, 'FOO'
+ end
+
+ def sort_records(_records)
+ raise NotImplementedError, 'FOO'
+ end
+
+ def paginate_records(_records)
+ raise NotImplementedError, 'FOO'
+ end
+ end
+ datatable.new(sample_params)
+ end
+
+ describe '#fetch_records' do
+ it 'calls #get_raw_records' do
+ allow(datatable).to receive(:get_raw_records) { User.all }
+ datatable.fetch_records
+ expect(datatable).to have_received(:get_raw_records)
+ end
+
+ it 'returns a collection of records' do
+ allow(datatable).to receive(:get_raw_records) { User.all }
+ expect(datatable.fetch_records).to be_a(ActiveRecord::Relation)
+ end
+ end
+
+ describe '#filter_records' do
+ it {
+ expect { datatable.filter_records([]) }.to raise_error(NotImplementedError).with_message('FOO')
+ }
+ end
+
+ describe '#sort_records' do
+ it {
+ expect { datatable.sort_records([]) }.to raise_error(NotImplementedError).with_message('FOO')
+ }
+ end
+
+ describe '#paginate_records' do
+ it {
+ expect { datatable.paginate_records([]) }.to raise_error(NotImplementedError).with_message('FOO')
+ }
+ end
+ end
+ end
+ end
+
+ describe 'JSON format' do
+ describe '#as_json' do
+ let(:datatable) { ComplexDatatable.new(sample_params) }
+
+ it 'returns a hash' do
+ create_list(:user, 5)
+ data = datatable.as_json
+ expect(data[:recordsTotal]).to eq 5
+ expect(data[:recordsFiltered]).to eq 5
+ expect(data[:draw]).to eq 1
+ expect(data[:data]).to be_a(Array)
+ expect(data[:data].size).to eq 5
+ end
+
+ context 'with additional_data' do
+ it 'returns a hash' do
+ create_list(:user, 5)
+ allow(datatable).to receive(:additional_data).and_return({ foo: 'bar' })
+ data = datatable.as_json
+ expect(data[:recordsTotal]).to eq 5
+ expect(data[:recordsFiltered]).to eq 5
+ expect(data[:draw]).to eq 1
+ expect(data[:data]).to be_a(Array)
+ expect(data[:data].size).to eq 5
+ expect(data[:foo]).to eq 'bar'
+ end
+ end
+ end
+ end
+
+ describe 'User helper methods' do
+ describe '#column_id' do
+ let(:datatable) { ComplexDatatable.new(sample_params) }
+
+ it 'returns column id from view_columns hash' do
+ expect(datatable.column_id(:username)).to eq(0)
+ expect(datatable.column_id('username')).to eq(0)
+ end
+ end
+
+ describe '#column_data' do
+ before { datatable.params[:columns]['0'][:search][:value] = 'doe' }
+
+ let(:datatable) { ComplexDatatable.new(sample_params) }
+
+ it 'returns column data from params' do
+ expect(datatable.column_data(:username)).to eq('doe')
+ expect(datatable.column_data('username')).to eq('doe')
+ end
+ end
+ end
+end
diff --git a/spec/ajax_datatables_rails/datatable/column_spec.rb b/spec/ajax_datatables_rails/datatable/column_spec.rb
new file mode 100644
index 00000000..21dc66f0
--- /dev/null
+++ b/spec/ajax_datatables_rails/datatable/column_spec.rb
@@ -0,0 +1,239 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe AjaxDatatablesRails::Datatable::Column do
+
+ let(:datatable) { ComplexDatatable.new(sample_params) }
+
+ describe 'username column' do
+
+ let(:column) { datatable.datatable.columns.first }
+
+ before { datatable.params[:columns]['0'][:search][:value] = 'searchvalue' }
+
+ it 'is orderable' do
+ expect(column.orderable?).to be(true)
+ end
+
+ it 'sorts nulls last' do
+ expect(column.nulls_last?).to be(false)
+ end
+
+ it 'is searchable' do
+ expect(column.searchable?).to be(true)
+ end
+
+ it 'is searched' do
+ expect(column.searched?).to be(true)
+ end
+
+ it 'has connected to id column' do
+ expect(column.data).to eq('username')
+ end
+
+ describe '#data' do
+ it 'returns the data from params' do
+ expect(column.data).to eq 'username'
+ end
+ end
+
+ describe '#source' do
+ it 'returns the data source from view_column' do
+ expect(column.source).to eq 'User.username'
+ end
+ end
+
+ describe '#table' do
+ context 'with ActiveRecord ORM' do
+ it 'returns the corresponding AR table' do
+ expect(column.table).to eq User.arel_table
+ end
+ end
+
+ context 'with other ORM' do
+ it 'returns the corresponding model' do
+ allow(User).to receive(:respond_to?).with(:arel_table).and_return(false)
+ expect(column.table).to eq User
+ end
+ end
+ end
+
+ describe '#model' do
+ it 'returns the corresponding AR model' do
+ expect(column.model).to eq User
+ end
+ end
+
+ describe '#field' do
+ it 'returns the corresponding field in DB' do
+ expect(column.field).to eq :username
+ end
+ end
+
+ describe '#custom_field?' do
+ it 'returns false if field is bound to an AR field' do
+ expect(column.custom_field?).to be false
+ end
+ end
+
+ describe '#search' do
+ it 'child class' do
+ expect(column.search).to be_a(AjaxDatatablesRails::Datatable::SimpleSearch)
+ end
+
+ it 'has search value' do
+ expect(column.search.value).to eq('searchvalue')
+ end
+
+ it 'does not regex' do
+ expect(column.search.regexp?).to be false
+ end
+ end
+
+ describe '#cond' do
+ it 'is :like by default' do
+ expect(column.cond).to eq(:like)
+ end
+ end
+
+ describe '#search_query' do
+ it 'bulds search query' do
+ expect(column.search_query.to_sql).to include('%searchvalue%')
+ end
+ end
+
+ describe '#sort_query' do
+ it 'builds sort query' do
+ expect(column.sort_query).to eq('users.username')
+ end
+ end
+
+ describe '#use_regex?' do
+ it 'is true by default' do
+ expect(column.use_regex?).to be true
+ end
+ end
+
+ describe '#delimiter' do
+ it 'is - by default' do
+ expect(column.delimiter).to eq('-')
+ end
+ end
+ end
+
+ describe 'unsearchable column' do
+ let(:column) { datatable.datatable.columns.find { |c| c.data == 'email_hash' } }
+
+ it 'is not searchable' do
+ expect(column.searchable?).to be(false)
+ end
+ end
+
+ describe '#formatter' do
+ let(:datatable) { DatatableWithFormater.new(sample_params) }
+ let(:column) { datatable.datatable.columns.find { |c| c.data == 'last_name' } }
+
+ it 'is a proc' do
+ expect(column.formatter).to be_a(Proc)
+ end
+ end
+
+ describe '#filter' do
+ let(:datatable) { DatatableCondProc.new(sample_params) }
+ let(:column) { datatable.datatable.columns.find { |c| c.data == 'username' } }
+
+ it 'is a proc' do
+ config = column.instance_variable_get(:@view_column)
+ filter = config[:cond]
+ expect(filter).to be_a(Proc)
+ allow(filter).to receive(:call).with(column, column.formatted_value)
+ column.filter
+ expect(filter).to have_received(:call).with(column, column.formatted_value)
+ end
+ end
+
+ describe '#type_cast' do
+ let(:column) { datatable.datatable.columns.first }
+
+ it 'returns VARCHAR if :db_adapter is :pg' do
+ allow(datatable).to receive(:db_adapter).and_return(:pg)
+ expect(column.send(:type_cast)).to eq('VARCHAR')
+ end
+
+ it 'returns VARCHAR if :db_adapter is :postgre' do
+ allow(datatable).to receive(:db_adapter).and_return(:postgre)
+ expect(column.send(:type_cast)).to eq('VARCHAR')
+ end
+
+ it 'returns VARCHAR if :db_adapter is :postgresql' do
+ allow(datatable).to receive(:db_adapter).and_return(:postgresql)
+ expect(column.send(:type_cast)).to eq('VARCHAR')
+ end
+
+ it 'returns VARCHAR if :db_adapter is :postgis' do
+ allow(datatable).to receive(:db_adapter).and_return(:postgis)
+ expect(column.send(:type_cast)).to eq('VARCHAR')
+ end
+
+ it 'returns VARCHAR2(4000) if :db_adapter is :oracle' do
+ allow(datatable).to receive(:db_adapter).and_return(:oracle)
+ expect(column.send(:type_cast)).to eq('VARCHAR2(4000)')
+ end
+
+ it 'returns VARCHAR2(4000) if :db_adapter is :oracleenhanced' do
+ allow(datatable).to receive(:db_adapter).and_return(:oracleenhanced)
+ expect(column.send(:type_cast)).to eq('VARCHAR2(4000)')
+ end
+
+ it 'returns CHAR if :db_adapter is :mysql2' do
+ allow(datatable).to receive(:db_adapter).and_return(:mysql2)
+ expect(column.send(:type_cast)).to eq('CHAR')
+ end
+
+ it 'returns CHAR if :db_adapter is :trilogy' do
+ allow(datatable).to receive(:db_adapter).and_return(:trilogy)
+ expect(column.send(:type_cast)).to eq('CHAR')
+ end
+
+ it 'returns CHAR if :db_adapter is :mysql' do
+ allow(datatable).to receive(:db_adapter).and_return(:mysql)
+ expect(column.send(:type_cast)).to eq('CHAR')
+ end
+
+ it 'returns TEXT if :db_adapter is :sqlite' do
+ allow(datatable).to receive(:db_adapter).and_return(:sqlite)
+ expect(column.send(:type_cast)).to eq('TEXT')
+ end
+
+ it 'returns TEXT if :db_adapter is :sqlite3' do
+ allow(datatable).to receive(:db_adapter).and_return(:sqlite3)
+ expect(column.send(:type_cast)).to eq('TEXT')
+ end
+
+ it 'returns VARCHAR(4000) if :db_adapter is :sqlserver' do
+ allow(datatable).to receive(:db_adapter).and_return(:sqlserver)
+ expect(column.send(:type_cast)).to eq('VARCHAR(4000)')
+ end
+ end
+
+ describe 'when empty column' do
+ before { datatable.params[:columns]['0'][:data] = '' }
+
+ let(:message) { 'Unknown column. Check that `data` field is filled on JS side with the column name' }
+
+ it 'raises error' do
+ expect { datatable.to_json }.to raise_error(AjaxDatatablesRails::Error::InvalidSearchColumn).with_message(message)
+ end
+ end
+
+ describe 'when unknown column' do
+ before { datatable.params[:columns]['0'][:data] = 'foo' }
+
+ let(:message) { "Check that column 'foo' exists in view_columns" }
+
+ it 'raises error' do
+ expect { datatable.to_json }.to raise_error(AjaxDatatablesRails::Error::InvalidSearchColumn).with_message(message)
+ end
+ end
+end
diff --git a/spec/ajax_datatables_rails/datatable/datatable_spec.rb b/spec/ajax_datatables_rails/datatable/datatable_spec.rb
new file mode 100644
index 00000000..fa12005d
--- /dev/null
+++ b/spec/ajax_datatables_rails/datatable/datatable_spec.rb
@@ -0,0 +1,128 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe AjaxDatatablesRails::Datatable::Datatable do
+
+ let(:datatable) { ComplexDatatable.new(sample_params).datatable }
+ let(:datatable_json) { ComplexDatatable.new(sample_params_json).datatable }
+ let(:order_option) { { '0' => { 'column' => '0', 'dir' => 'asc' }, '1' => { 'column' => '1', 'dir' => 'desc' } } }
+ let(:order_option_json) { [{ 'column' => '0', 'dir' => 'asc' }, { 'column' => '1', 'dir' => 'desc' }] }
+
+ shared_examples 'order methods' do
+ it 'is orderable' do
+ expect(datatable.orderable?).to be(true)
+ end
+
+ it 'is not orderable' do
+ datatable.options[:order] = nil
+ expect(datatable.orderable?).to be(false)
+ end
+
+ it 'has 2 orderable columns' do
+ datatable.options[:order] = order_option
+ expect(datatable.orders.count).to eq(2)
+ end
+
+ it 'first column ordered by ASC' do
+ datatable.options[:order] = order_option
+ expect(datatable.orders.first.direction).to eq('ASC')
+ end
+
+ it 'first column ordered by DESC' do
+ datatable.options[:order] = order_option
+ expect(datatable.orders.last.direction).to eq('DESC')
+ end
+
+ it 'child class' do
+ expect(datatable.orders.first).to be_a(AjaxDatatablesRails::Datatable::SimpleOrder)
+ end
+ end
+
+ shared_examples 'columns methods' do
+ it 'has 8 columns' do
+ expect(datatable.columns.count).to eq(8)
+ end
+
+ it 'child class' do
+ expect(datatable.columns.first).to be_a(AjaxDatatablesRails::Datatable::Column)
+ end
+ end
+
+ describe 'with query params' do
+ it_behaves_like 'order methods'
+ it_behaves_like 'columns methods'
+ end
+
+ describe 'with json params' do
+ let(:order_option) { order_option_json }
+ let(:datatable) { datatable_json }
+
+ it_behaves_like 'order methods'
+ it_behaves_like 'columns methods'
+ end
+
+ describe 'search methods' do
+ it 'is searchable' do
+ datatable.options[:search][:value] = 'atom'
+ expect(datatable.searchable?).to be(true)
+ end
+
+ it 'is not searchable' do
+ datatable.options[:search][:value] = nil
+ expect(datatable.searchable?).to be(false)
+ end
+
+ it 'child class' do
+ expect(datatable.search).to be_a(AjaxDatatablesRails::Datatable::SimpleSearch)
+ end
+ end
+
+ describe 'option methods' do
+ describe '#paginate?' do
+ it {
+ expect(datatable.paginate?).to be(true)
+ }
+ end
+
+ describe '#per_page' do
+ context 'when params[:length] is missing' do
+ it 'defaults to 10' do
+ expect(datatable.per_page).to eq(10)
+ end
+ end
+
+ context 'when params[:length] is passed' do
+ let(:datatable) { ComplexDatatable.new({ length: '20' }).datatable }
+
+ it 'matches the value on view params[:length]' do
+ expect(datatable.per_page).to eq(20)
+ end
+ end
+ end
+
+ describe '#offset' do
+ context 'when params[:start] is missing' do
+ it 'defaults to 0' do
+ expect(datatable.offset).to eq(0)
+ end
+ end
+
+ context 'when params[:start] is passed' do
+ let(:datatable) { ComplexDatatable.new({ start: '11' }).datatable }
+
+ it 'matches the value on view params[:start]' do
+ expect(datatable.offset).to eq(11)
+ end
+ end
+ end
+
+ describe '#page' do
+ let(:datatable) { ComplexDatatable.new({ start: '11' }).datatable }
+
+ it 'calculates page number from params[:start] and #per_page' do
+ expect(datatable.page).to eq(2)
+ end
+ end
+ end
+end
diff --git a/spec/ajax_datatables_rails/datatable/simple_order_spec.rb b/spec/ajax_datatables_rails/datatable/simple_order_spec.rb
new file mode 100644
index 00000000..36d2260f
--- /dev/null
+++ b/spec/ajax_datatables_rails/datatable/simple_order_spec.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe AjaxDatatablesRails::Datatable::SimpleOrder do
+
+ let(:parent) { ComplexDatatable.new(sample_params) }
+ let(:datatable) { parent.datatable }
+ let(:options) { ActiveSupport::HashWithIndifferentAccess.new({ 'column' => '1', 'dir' => 'desc' }) }
+ let(:simple_order) { described_class.new(datatable, options) }
+
+ describe 'option methods' do
+ it 'sql query' do
+ expect(simple_order.query('firstname')).to eq('firstname DESC')
+ end
+ end
+
+ describe 'option methods with nulls last' do
+ describe 'using class option' do
+ before { parent.nulls_last = true }
+ after { parent.nulls_last = false }
+
+ it 'sql query' do
+ skip('unsupported database adapter') if RunningSpec.oracle?
+
+ expect(simple_order.query('email')).to eq(
+ "email DESC #{nulls_last_sql(parent)}"
+ )
+ end
+ end
+
+ describe 'using column option' do
+ let(:parent) { DatatableOrderNullsLast.new(sample_params) }
+ let(:sorted_datatable) { parent.datatable }
+ let(:nulls_last_order) { described_class.new(sorted_datatable, options) }
+
+ context 'with postgres database adapter' do
+ before { parent.db_adapter = :pg }
+
+ it 'sql query' do
+ expect(nulls_last_order.query('email')).to eq('email DESC NULLS LAST')
+ end
+ end
+
+ context 'with postgis database adapter' do
+ before { parent.db_adapter = :postgis }
+
+ it 'sql query' do
+ expect(nulls_last_order.query('email')).to eq('email DESC NULLS LAST')
+ end
+ end
+
+ context 'with sqlite database adapter' do
+ before { parent.db_adapter = :sqlite }
+
+ it 'sql query' do
+ expect(nulls_last_order.query('email')).to eq('email DESC IS NULL')
+ end
+ end
+
+ context 'with mysql database adapter' do
+ before { parent.db_adapter = :mysql }
+
+ it 'sql query' do
+ expect(nulls_last_order.query('email')).to eq('email DESC IS NULL')
+ end
+ end
+ end
+ end
+end
diff --git a/spec/ajax-datatables-rails/datatable/simple_search_spec.rb b/spec/ajax_datatables_rails/datatable/simple_search_spec.rb
similarity index 59%
rename from spec/ajax-datatables-rails/datatable/simple_search_spec.rb
rename to spec/ajax_datatables_rails/datatable/simple_search_spec.rb
index b9eb1fbf..a13bed72 100644
--- a/spec/ajax-datatables-rails/datatable/simple_search_spec.rb
+++ b/spec/ajax_datatables_rails/datatable/simple_search_spec.rb
@@ -1,9 +1,11 @@
+# frozen_string_literal: true
+
require 'spec_helper'
-describe AjaxDatatablesRails::Datatable::SimpleSearch do
+RSpec.describe AjaxDatatablesRails::Datatable::SimpleSearch do
- let(:options) { ActiveSupport::HashWithIndifferentAccess.new({'value'=>'search value', 'regex'=>'true'}) }
- let(:simple_search) { AjaxDatatablesRails::Datatable::SimpleSearch.new(options) }
+ let(:options) { ActiveSupport::HashWithIndifferentAccess.new({ 'value' => 'search value', 'regex' => 'true' }) }
+ let(:simple_search) { described_class.new(options) }
describe 'option methods' do
it 'regexp?' do
diff --git a/spec/ajax_datatables_rails/orm/active_record_count_records_spec.rb b/spec/ajax_datatables_rails/orm/active_record_count_records_spec.rb
new file mode 100644
index 00000000..70c4de4c
--- /dev/null
+++ b/spec/ajax_datatables_rails/orm/active_record_count_records_spec.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe AjaxDatatablesRails::ORM::ActiveRecord do
+
+ let(:datatable) { ComplexDatatable.new(sample_params) }
+ let(:records) { User.all }
+
+ describe '#records_total_count' do
+ context 'when ungrouped results' do
+ it 'returns the count' do
+ expect(datatable.send(:records_total_count)).to eq records.count
+ end
+ end
+
+ context 'when grouped results' do
+ let(:datatable) { GroupedDatatable.new(sample_params) }
+
+ it 'returns the count' do
+ expect(datatable.send(:records_total_count)).to eq records.count
+ end
+ end
+ end
+
+ describe '#records_filtered_count' do
+ context 'when ungrouped results' do
+ it 'returns the count' do
+ expect(datatable.send(:records_filtered_count)).to eq records.count
+ end
+ end
+
+ context 'when grouped results' do
+ let(:datatable) { GroupedDatatable.new(sample_params) }
+
+ it 'returns the count' do
+ expect(datatable.send(:records_filtered_count)).to eq records.count
+ end
+ end
+ end
+end
diff --git a/spec/ajax_datatables_rails/orm/active_record_filter_records_spec.rb b/spec/ajax_datatables_rails/orm/active_record_filter_records_spec.rb
new file mode 100644
index 00000000..4bcbd541
--- /dev/null
+++ b/spec/ajax_datatables_rails/orm/active_record_filter_records_spec.rb
@@ -0,0 +1,662 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe AjaxDatatablesRails::ORM::ActiveRecord do
+
+ let(:datatable) { ComplexDatatable.new(sample_params) }
+ let(:records) { User.all }
+
+ describe '#filter_records' do
+ it 'requires a records collection as argument' do
+ expect { datatable.filter_records }.to raise_error(ArgumentError)
+ end
+
+ context 'with simple search' do
+ before do
+ datatable.params[:search] = { value: 'msmith' }
+ end
+
+ it 'performs a simple search first' do
+ allow(datatable).to receive(:build_conditions_for_datatable)
+ datatable.filter_records(records)
+ expect(datatable).to have_received(:build_conditions_for_datatable)
+ end
+
+ it 'does not search unsearchable fields' do
+ criteria = datatable.filter_records(records)
+ expect(criteria.to_sql).to_not include('email_hash')
+ end
+ end
+
+ it 'performs a composite search second' do
+ datatable.params[:search] = { value: '' }
+ allow(datatable).to receive(:build_conditions_for_selected_columns)
+ datatable.filter_records(records)
+ expect(datatable).to have_received(:build_conditions_for_selected_columns)
+ end
+ end
+
+ describe '#build_conditions' do
+ before do
+ create(:user, username: 'johndoe', email: 'johndoe@example.com')
+ create(:user, username: 'msmith', email: 'mary.smith@example.com')
+ create(:user, username: 'hsmith', email: 'henry.smith@example.net')
+ end
+
+ context 'with column and global search' do
+ before do
+ datatable.params[:search] = { value: 'example.com', regex: 'false' }
+ datatable.params[:columns]['0'][:search][:value] = 'smith'
+ end
+
+ it 'return a filtered set of records' do
+ query = datatable.build_conditions
+ results = records.where(query).map(&:username)
+ expect(results).to include('msmith')
+ expect(results).to_not include('johndoe')
+ expect(results).to_not include('hsmith')
+ end
+ end
+ end
+
+ describe '#build_conditions_for_datatable' do
+ before do
+ create(:user, username: 'johndoe', email: 'johndoe@example.com')
+ create(:user, username: 'msmith', email: 'mary.smith@example.com')
+ end
+
+ it 'returns an Arel object' do
+ datatable.params[:search] = { value: 'msmith' }
+ result = datatable.build_conditions_for_datatable
+ expect(result).to be_a(Arel::Nodes::Grouping)
+ end
+
+ context 'when no search query' do
+ it 'returns empty query' do
+ datatable.params[:search] = { value: '' }
+ expect(datatable.build_conditions_for_datatable).to be_blank
+ end
+ end
+
+ context 'when none of columns are connected' do
+ before do
+ allow(datatable).to receive(:searchable_columns).and_return([])
+ end
+
+ context 'when search value is a string' do
+ before do
+ datatable.params[:search] = { value: 'msmith' }
+ end
+
+ it 'returns empty query result' do
+ expect(datatable.build_conditions_for_datatable).to be_blank
+ end
+
+ it 'returns filtered results' do
+ query = datatable.build_conditions_for_datatable
+ results = records.where(query).map(&:username)
+ expect(results).to eq %w[johndoe msmith]
+ end
+ end
+
+ context 'when search value is space-separated string' do
+ before do
+ datatable.params[:search] = { value: 'foo bar' }
+ end
+
+ it 'returns empty query result' do
+ expect(datatable.build_conditions_for_datatable).to be_blank
+ end
+
+ it 'returns filtered results' do
+ query = datatable.build_conditions_for_datatable
+ results = records.where(query).map(&:username)
+ expect(results).to eq %w[johndoe msmith]
+ end
+ end
+ end
+
+ context 'with search query' do
+ context 'when search value is a string' do
+ before do
+ datatable.params[:search] = { value: 'john', regex: 'false' }
+ end
+
+ it 'returns a filtering query' do
+ query = datatable.build_conditions_for_datatable
+ results = records.where(query).map(&:username)
+ expect(results).to include('johndoe')
+ expect(results).to_not include('msmith')
+ end
+ end
+
+ context 'when search value is space-separated string' do
+ before do
+ datatable.params[:search] = { value: 'john doe', regex: 'false' }
+ end
+
+ it 'returns a filtering query' do
+ query = datatable.build_conditions_for_datatable
+ results = records.where(query).map(&:username)
+ expect(results).to eq ['johndoe']
+ expect(results).to_not include('msmith')
+ end
+ end
+
+ # TODO: improve (or delete?) this test
+ context 'when column.search_query returns nil' do
+ let(:datatable) { DatatableCondUnknown.new(sample_params) }
+
+ before do
+ datatable.params[:search] = { value: 'john doe', regex: 'false' }
+ end
+
+ it 'does not raise error' do
+ allow_any_instance_of(AjaxDatatablesRails::Datatable::Column).to receive(:valid_search_condition?).and_return(true) # rubocop:disable RSpec/AnyInstance
+
+ expect {
+ datatable.data.size
+ }.to_not raise_error
+ end
+ end
+ end
+ end
+
+ describe '#build_conditions_for_selected_columns' do
+ before do
+ create(:user, username: 'johndoe', email: 'johndoe@example.com')
+ create(:user, username: 'msmith', email: 'mary.smith@example.com')
+ end
+
+ context 'when columns include search query' do
+ before do
+ datatable.params[:columns]['0'][:search][:value] = 'doe'
+ datatable.params[:columns]['1'][:search][:value] = 'example'
+ end
+
+ it 'returns an Arel object' do
+ result = datatable.build_conditions_for_selected_columns
+ expect(result).to be_a(Arel::Nodes::And)
+ end
+
+ if RunningSpec.postgresql?
+ context 'when db_adapter is postgresql' do
+ it 'can call #to_sql on returned object' do
+ result = datatable.build_conditions_for_selected_columns
+ expect(result).to respond_to(:to_sql)
+ expect(result.to_sql).to eq(
+ "CAST(\"users\".\"username\" AS VARCHAR) ILIKE '%doe%' AND CAST(\"users\".\"email\" AS VARCHAR) ILIKE '%example%'"
+ )
+ end
+ end
+ end
+
+ if RunningSpec.oracle?
+ context 'when db_adapter is oracle' do
+ it 'can call #to_sql on returned object' do
+ result = datatable.build_conditions_for_selected_columns
+ expect(result).to respond_to(:to_sql)
+ expect(result.to_sql).to eq(
+ "UPPER(CAST(\"USERS\".\"USERNAME\" AS VARCHAR2(4000))) LIKE UPPER('%doe%') AND UPPER(CAST(\"USERS\".\"EMAIL\" AS VARCHAR2(4000))) LIKE UPPER('%example%')" # rubocop:disable Layout/LineLength
+ )
+ end
+ end
+ end
+
+ if RunningSpec.mysql?
+ context 'when db_adapter is mysql2' do # rubocop:disable RSpec/RepeatedExampleGroupBody
+ it 'can call #to_sql on returned object' do
+ result = datatable.build_conditions_for_selected_columns
+ expect(result).to respond_to(:to_sql)
+ expect(result.to_sql).to eq(
+ "CAST(`users`.`username` AS CHAR) LIKE '%doe%' AND CAST(`users`.`email` AS CHAR) LIKE '%example%'"
+ )
+ end
+ end
+
+ context 'when db_adapter is trilogy' do # rubocop:disable RSpec/RepeatedExampleGroupBody
+ it 'can call #to_sql on returned object' do
+ result = datatable.build_conditions_for_selected_columns
+ expect(result).to respond_to(:to_sql)
+ expect(result.to_sql).to eq(
+ "CAST(`users`.`username` AS CHAR) LIKE '%doe%' AND CAST(`users`.`email` AS CHAR) LIKE '%example%'"
+ )
+ end
+ end
+ end
+ end
+
+ it 'calls #build_conditions_for_selected_columns' do
+ allow(datatable).to receive(:build_conditions_for_selected_columns)
+ datatable.build_conditions
+ expect(datatable).to have_received(:build_conditions_for_selected_columns)
+ end
+
+ context 'with search values in columns' do
+ before do
+ datatable.params[:columns]['0'][:search][:value] = 'doe'
+ end
+
+ it 'returns a filtered set of records' do
+ query = datatable.build_conditions_for_selected_columns
+ results = records.where(query).map(&:username)
+ expect(results).to include('johndoe')
+ expect(results).to_not include('msmith')
+ end
+ end
+ end
+
+ describe 'filter conditions' do
+ context 'with date condition' do
+ describe 'it can filter records with condition :date_range' do
+ let(:datatable) { DatatableCondDate.new(sample_params) }
+
+ before do
+ create(:user, username: 'johndoe', email: 'johndoe@example.com', last_name: 'Doe', created_at: '01/01/2000')
+ create(:user, username: 'msmith', email: 'mary.smith@example.com', last_name: 'Smith', created_at: '01/02/2000')
+ end
+
+ context 'when range is empty' do
+ it 'does not filter records' do
+ datatable.params[:columns]['7'][:search][:value] = '-'
+ expect(datatable.data.size).to eq 2
+ item = datatable.data.first
+ expect(item[:last_name]).to eq 'Doe'
+ end
+ end
+
+ context 'when start date is filled' do
+ it 'filters records created after this date' do
+ datatable.params[:columns]['7'][:search][:value] = '31/12/1999-'
+ expect(datatable.data.size).to eq 2
+ end
+ end
+
+ context 'when end date is filled' do
+ it 'filters records created before this date' do
+ datatable.params[:columns]['7'][:search][:value] = '-31/12/1999'
+ expect(datatable.data.size).to eq 0
+ end
+ end
+
+ context 'when both date are filled' do
+ it 'filters records created between the range' do
+ datatable.params[:columns]['7'][:search][:value] = '01/12/1999-15/01/2000'
+ expect(datatable.data.size).to eq 1
+ end
+ end
+
+ context 'when another filter is active' do
+ context 'when range is empty' do
+ it 'filters records' do
+ datatable.params[:columns]['0'][:search][:value] = 'doe'
+ datatable.params[:columns]['7'][:search][:value] = '-'
+ expect(datatable.data.size).to eq 1
+ item = datatable.data.first
+ expect(item[:last_name]).to eq 'Doe'
+ end
+ end
+
+ context 'when start date is filled' do
+ it 'filters records' do
+ datatable.params[:columns]['0'][:search][:value] = 'doe'
+ datatable.params[:columns]['7'][:search][:value] = '01/12/1999-'
+ expect(datatable.data.size).to eq 1
+ item = datatable.data.first
+ expect(item[:last_name]).to eq 'Doe'
+ end
+ end
+
+ context 'when end date is filled' do
+ it 'filters records' do
+ datatable.params[:columns]['0'][:search][:value] = 'doe'
+ datatable.params[:columns]['7'][:search][:value] = '-15/01/2000'
+ expect(datatable.data.size).to eq 1
+ item = datatable.data.first
+ expect(item[:last_name]).to eq 'Doe'
+ end
+ end
+
+ context 'when both date are filled' do
+ it 'filters records' do
+ datatable.params[:columns]['0'][:search][:value] = 'doe'
+ datatable.params[:columns]['7'][:search][:value] = '01/12/1999-15/01/2000'
+ expect(datatable.data.size).to eq 1
+ item = datatable.data.first
+ expect(item[:last_name]).to eq 'Doe'
+ end
+ end
+ end
+ end
+ end
+
+ context 'with numeric condition' do
+ before do
+ create(:user, first_name: 'john', post_id: 1)
+ create(:user, first_name: 'mary', post_id: 2)
+ end
+
+ describe 'it can filter records with condition :eq' do
+ let(:datatable) { DatatableCondEq.new(sample_params) }
+
+ it 'filters records matching' do
+ datatable.params[:columns]['5'][:search][:value] = 1
+ expect(datatable.data.size).to eq 1
+ item = datatable.data.first
+ expect(item[:first_name]).to eq 'john'
+ end
+ end
+
+ describe 'it can filter records with condition :not_eq' do
+ let(:datatable) { DatatableCondNotEq.new(sample_params) }
+
+ it 'filters records matching' do
+ datatable.params[:columns]['5'][:search][:value] = 1
+ expect(datatable.data.size).to eq 1
+ item = datatable.data.first
+ expect(item[:first_name]).to eq 'mary'
+ end
+ end
+
+ describe 'it can filter records with condition :lt' do
+ let(:datatable) { DatatableCondLt.new(sample_params) }
+
+ it 'filters records matching' do
+ datatable.params[:columns]['5'][:search][:value] = 2
+ expect(datatable.data.size).to eq 1
+ item = datatable.data.first
+ expect(item[:first_name]).to eq 'john'
+ end
+ end
+
+ describe 'it can filter records with condition :gt' do
+ let(:datatable) { DatatableCondGt.new(sample_params) }
+
+ it 'filters records matching' do
+ datatable.params[:columns]['5'][:search][:value] = 1
+ expect(datatable.data.size).to eq 1
+ item = datatable.data.first
+ expect(item[:first_name]).to eq 'mary'
+ end
+ end
+
+ describe 'it can filter records with condition :lteq' do
+ let(:datatable) { DatatableCondLteq.new(sample_params) }
+
+ it 'filters records matching' do
+ datatable.params[:columns]['5'][:search][:value] = 2
+ expect(datatable.data.size).to eq 2
+ end
+ end
+
+ describe 'it can filter records with condition :gteq' do
+ let(:datatable) { DatatableCondGteq.new(sample_params) }
+
+ it 'filters records matching' do
+ datatable.params[:columns]['5'][:search][:value] = 1
+ expect(datatable.data.size).to eq 2
+ end
+ end
+
+ describe 'it can filter records with condition :in' do
+ let(:datatable) { DatatableCondIn.new(sample_params) }
+
+ it 'filters records matching' do
+ datatable.params[:columns]['5'][:search][:value] = [1]
+ expect(datatable.data.size).to eq 1
+ item = datatable.data.first
+ expect(item[:first_name]).to eq 'john'
+ end
+ end
+
+ describe 'it can filter records with condition :in with regex' do
+ let(:datatable) { DatatableCondInWithRegex.new(sample_params) }
+
+ it 'filters records matching' do
+ datatable.params[:columns]['5'][:search][:value] = '1|2'
+ datatable.params[:order]['0'] = { column: '5', dir: 'asc' }
+ expect(datatable.data.size).to eq 2
+ item = datatable.data.first
+ expect(item[:first_name]).to eq 'john'
+ end
+ end
+
+ describe 'Integer overflows' do
+ let(:datatable) { DatatableCondEq.new(sample_params) }
+ let(:largest_postgresql_integer_value) { 2_147_483_647 }
+ let(:smallest_postgresql_integer_value) { -2_147_483_648 }
+
+ before do
+ create(:user, first_name: 'john', post_id: 1)
+ create(:user, first_name: 'mary', post_id: 2)
+ create(:user, first_name: 'phil', post_id: largest_postgresql_integer_value)
+ end
+
+ it 'returns an empty result if input value is too large' do
+ datatable.params[:columns]['5'][:search][:value] = largest_postgresql_integer_value + 1
+ expect(datatable.data.size).to eq 0
+ end
+
+ it 'returns an empty result if input value is too small' do
+ datatable.params[:columns]['5'][:search][:value] = smallest_postgresql_integer_value - 1
+ expect(datatable.data.size).to eq 0
+ end
+
+ it 'returns the matching user' do
+ datatable.params[:columns]['5'][:search][:value] = largest_postgresql_integer_value
+ expect(datatable.data.size).to eq 1
+ end
+ end
+ end
+
+ context 'with proc condition' do
+ describe 'it can filter records with lambda/proc condition' do
+ let(:datatable) { DatatableCondProc.new(sample_params) }
+
+ before do
+ create(:user, username: 'johndoe', email: 'johndoe@example.com')
+ create(:user, username: 'johndie', email: 'johndie@example.com')
+ create(:user, username: 'msmith', email: 'mary.smith@example.com')
+ end
+
+ it 'filters records matching' do
+ datatable.params[:columns]['0'][:search][:value] = 'john'
+ expect(datatable.data.size).to eq 2
+ item = datatable.data.first
+ expect(item[:username]).to eq 'johndie'
+ end
+ end
+ end
+
+ context 'with string condition' do
+ describe 'it can filter records with condition :start_with' do
+ let(:datatable) { DatatableCondStartWith.new(sample_params) }
+
+ before do
+ create(:user, first_name: 'John')
+ create(:user, first_name: 'Mary')
+ end
+
+ it 'filters records matching' do
+ datatable.params[:columns]['2'][:search][:value] = 'Jo'
+ expect(datatable.data.size).to eq 1
+ item = datatable.data.first
+ expect(item[:first_name]).to eq 'John'
+ end
+ end
+
+ describe 'it can filter records with condition :end_with' do
+ let(:datatable) { DatatableCondEndWith.new(sample_params) }
+
+ before do
+ create(:user, last_name: 'JOHN')
+ create(:user, last_name: 'MARY')
+ end
+
+ if RunningSpec.oracle?
+ context 'when db_adapter is oracleenhanced' do
+ it 'filters records matching' do
+ datatable.params[:columns]['3'][:search][:value] = 'RY'
+ expect(datatable.data.size).to eq 1
+ item = datatable.data.first
+ expect(item[:last_name]).to eq 'MARY'
+ end
+ end
+ else
+ it 'filters records matching' do
+ datatable.params[:columns]['3'][:search][:value] = 'ry'
+ expect(datatable.data.size).to eq 1
+ item = datatable.data.first
+ expect(item[:last_name]).to eq 'MARY'
+ end
+ end
+ end
+
+ describe 'it can filter records with condition :like' do
+ let(:datatable) { DatatableCondLike.new(sample_params) }
+
+ before do
+ create(:user, email: 'john@foo.com')
+ create(:user, email: 'mary@bar.com')
+ end
+
+ it 'filters records matching' do
+ datatable.params[:columns]['1'][:search][:value] = 'foo'
+ expect(datatable.data.size).to eq 1
+ item = datatable.data.first
+ expect(item[:email]).to eq 'john@foo.com'
+ end
+ end
+
+ describe 'it can filter records with condition :string_eq' do
+ let(:datatable) { DatatableCondStringEq.new(sample_params) }
+
+ before do
+ create(:user, email: 'john@foo.com')
+ create(:user, email: 'mary@bar.com')
+ end
+
+ it 'filters records matching' do
+ datatable.params[:columns]['1'][:search][:value] = 'john@foo.com'
+ expect(datatable.data.size).to eq 1
+ item = datatable.data.first
+ expect(item[:email]).to eq 'john@foo.com'
+ end
+ end
+
+ describe 'it can filter records with condition :string_in' do
+ let(:datatable) { DatatableCondStringIn.new(sample_params) }
+
+ before do
+ create(:user, email: 'john@foo.com')
+ create(:user, email: 'mary@bar.com')
+ create(:user, email: 'henry@baz.com')
+ end
+
+ it 'filters records matching' do
+ datatable.params[:columns]['1'][:search][:value] = 'john@foo.com'
+ expect(datatable.data.size).to eq 1
+ item = datatable.data.first
+ expect(item[:email]).to eq 'john@foo.com'
+ end
+
+ it 'filters records matching with multiple' do
+ datatable.params[:columns]['1'][:search][:value] = 'john@foo.com|henry@baz.com'
+ expect(datatable.data.size).to eq 2
+ items = datatable.data.sort_by { |h| h[:email] }
+ item_first = items.first
+ item_last = items.last
+ expect(item_first[:email]).to eq 'henry@baz.com'
+ expect(item_last[:email]).to eq 'john@foo.com'
+ end
+
+ it 'filters records matching with multiple contains not found' do
+ datatable.params[:columns]['1'][:search][:value] = 'john@foo.com|henry_not@baz.com'
+ expect(datatable.data.size).to eq 1
+ item = datatable.data.first
+ expect(item[:email]).to eq 'john@foo.com'
+ end
+ end
+
+ describe 'it can filter records with condition :null_value' do
+ let(:datatable) { DatatableCondNullValue.new(sample_params) }
+
+ before do
+ create(:user, first_name: 'john', email: 'foo@bar.com')
+ create(:user, first_name: 'mary', email: nil)
+ end
+
+ context 'when condition is NULL' do
+ it 'filters records matching' do
+ datatable.params[:columns]['1'][:search][:value] = 'NULL'
+ expect(datatable.data.size).to eq 1
+ item = datatable.data.first
+ expect(item[:first_name]).to eq 'mary'
+ end
+ end
+
+ context 'when condition is !NULL' do
+ it 'filters records matching' do
+ datatable.params[:columns]['1'][:search][:value] = '!NULL'
+ expect(datatable.data.size).to eq 1
+ item = datatable.data.first
+ expect(item[:first_name]).to eq 'john'
+ end
+ end
+ end
+ end
+
+ context 'with unknown condition' do
+ let(:datatable) { DatatableCondUnknown.new(sample_params) }
+
+ before do
+ datatable.params[:search] = { value: 'john doe', regex: 'false' }
+ end
+
+ it 'raises error' do
+ expect {
+ datatable.data.size
+ }.to raise_error(AjaxDatatablesRails::Error::InvalidSearchCondition).with_message('foo')
+ end
+ end
+
+ context 'with custom column' do
+ describe 'it can filter records with custom column' do
+ let(:datatable) { DatatableCustomColumn.new(sample_params) }
+
+ before do
+ create(:user, username: 'msmith', email: 'mary.smith@example.com', first_name: 'Mary', last_name: 'Smith')
+ create(:user, username: 'jsmith', email: 'john.smith@example.com', first_name: 'John', last_name: 'Smith')
+ create(:user, username: 'johndoe', email: 'johndoe@example.com', first_name: 'John', last_name: 'Doe')
+ end
+
+ it 'filters records' do
+ skip('unsupported database adapter') if RunningSpec.oracle? || RunningSpec.sqlite?
+
+ datatable.params[:columns]['4'][:search][:value] = 'John'
+ datatable.params[:order]['0'][:column] = '4'
+ expect(datatable.data.size).to eq 2
+ item = datatable.data.first
+ expect(item[:full_name]).to eq 'John Doe'
+ end
+ end
+ end
+ end
+
+ describe 'formatter option' do
+ let(:datatable) { DatatableWithFormater.new(sample_params) }
+
+ before do
+ create(:user, username: 'johndoe', email: 'johndoe@example.com', last_name: 'DOE')
+ create(:user, username: 'msmith', email: 'mary.smith@example.com', last_name: 'SMITH')
+ datatable.params[:columns]['3'][:search][:value] = 'doe'
+ end
+
+ it 'can transform search value before asking the database' do
+ expect(datatable.data.size).to eq 1
+ item = datatable.data.first
+ expect(item[:last_name]).to eq 'DOE'
+ end
+ end
+end
diff --git a/spec/ajax-datatables-rails/orm/active_record_paginate_records_spec.rb b/spec/ajax_datatables_rails/orm/active_record_paginate_records_spec.rb
similarity index 64%
rename from spec/ajax-datatables-rails/orm/active_record_paginate_records_spec.rb
rename to spec/ajax_datatables_rails/orm/active_record_paginate_records_spec.rb
index d9ddaa3b..a33c834d 100644
--- a/spec/ajax-datatables-rails/orm/active_record_paginate_records_spec.rb
+++ b/spec/ajax_datatables_rails/orm/active_record_paginate_records_spec.rb
@@ -1,12 +1,13 @@
+# frozen_string_literal: true
+
require 'spec_helper'
-describe AjaxDatatablesRails::ORM::ActiveRecord do
+RSpec.describe AjaxDatatablesRails::ORM::ActiveRecord do
- let(:view) { double('view', params: sample_params) }
- let(:datatable) { ComplexDatatable.new(view) }
+ let(:datatable) { ComplexDatatable.new(sample_params) }
let(:records) { User.all }
- before(:each) do
+ before do
create(:user, username: 'johndoe', email: 'johndoe@example.com')
create(:user, username: 'msmith', email: 'mary.smith@example.com')
end
@@ -16,9 +17,9 @@
expect { datatable.paginate_records }.to raise_error(ArgumentError)
end
- it 'paginates records properly' do
- if AjaxDatatablesRails.config.db_adapter.in? %i[oracle oracleenhanced]
- if Rails.version.in? %w[4.0.13 4.1.15 4.2.9]
+ it 'paginates records properly' do # rubocop:disable RSpec/ExampleLength
+ if RunningSpec.oracle?
+ if Rails.version.in? %w[4.2.11]
expect(datatable.paginate_records(records).to_sql).to include(
'rownum <= 10'
)
@@ -35,31 +36,33 @@
datatable.params[:start] = '26'
datatable.params[:length] = '25'
- if AjaxDatatablesRails.config.db_adapter.in? %i[oracle oracleenhanced]
- if Rails.version.in? %w[4.0.13 4.1.15 4.2.9]
+ if RunningSpec.oracle?
+ if Rails.version.in? %w[4.2.11]
expect(datatable.paginate_records(records).to_sql).to include(
- 'rownum <= 50'
+ 'rownum <= 51'
)
else
expect(datatable.paginate_records(records).to_sql).to include(
- 'rownum <= (25 + 25)'
+ 'rownum <= (26 + 25)'
)
end
else
expect(datatable.paginate_records(records).to_sql).to include(
- 'LIMIT 25 OFFSET 25'
+ 'LIMIT 25 OFFSET 26'
)
end
end
it 'depends on the value of #offset' do
- expect(datatable.datatable).to receive(:offset)
+ allow(datatable.datatable).to receive(:offset)
datatable.paginate_records(records)
+ expect(datatable.datatable).to have_received(:offset)
end
it 'depends on the value of #per_page' do
- expect(datatable.datatable).to receive(:per_page).at_least(:once) { 10 }
+ allow(datatable.datatable).to receive(:per_page).at_least(:once).and_return(10)
datatable.paginate_records(records)
+ expect(datatable.datatable).to have_received(:per_page).at_least(:once)
end
end
diff --git a/spec/ajax_datatables_rails/orm/active_record_sort_records_spec.rb b/spec/ajax_datatables_rails/orm/active_record_sort_records_spec.rb
new file mode 100644
index 00000000..369d859c
--- /dev/null
+++ b/spec/ajax_datatables_rails/orm/active_record_sort_records_spec.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe AjaxDatatablesRails::ORM::ActiveRecord do
+
+ let(:datatable) { ComplexDatatable.new(sample_params) }
+ let(:nulls_last_datatable) { DatatableOrderNullsLast.new(sample_params) }
+ let(:records) { User.all }
+
+ before do
+ create(:user, username: 'johndoe', email: 'johndoe@example.com')
+ create(:user, username: 'msmith', email: 'mary.smith@example.com')
+ end
+
+ describe '#sort_records' do
+ it 'returns a records collection sorted by :order params' do
+ # set to order Users by email in descending order
+ datatable.params[:order]['0'] = { column: '1', dir: 'desc' }
+ expect(datatable.sort_records(records).map(&:email)).to match(
+ ['mary.smith@example.com', 'johndoe@example.com']
+ )
+ end
+
+ it 'can handle multiple sorting columns' do
+ # set to order by Users username in ascending order, and
+ # by Users email in descending order
+ datatable.params[:order]['0'] = { column: '0', dir: 'asc' }
+ datatable.params[:order]['1'] = { column: '1', dir: 'desc' }
+ expect(datatable.sort_records(records).to_sql).to include(
+ 'ORDER BY users.username ASC, users.email DESC'
+ )
+ end
+
+ it 'does not sort a column which is not orderable' do
+ datatable.params[:order]['0'] = { column: '0', dir: 'asc' }
+ datatable.params[:order]['1'] = { column: '4', dir: 'desc' }
+
+ expect(datatable.sort_records(records).to_sql).to include(
+ 'ORDER BY users.username ASC'
+ )
+
+ expect(datatable.sort_records(records).to_sql).to_not include(
+ 'users.post_id DESC'
+ )
+ end
+ end
+
+ describe '#sort_records with nulls last using global config' do
+ before { datatable.nulls_last = true }
+ after { datatable.nulls_last = false }
+
+ it 'can handle multiple sorting columns' do
+ skip('unsupported database adapter') if RunningSpec.oracle?
+
+ # set to order by Users username in ascending order, and
+ # by Users email in descending order
+ datatable.params[:order]['0'] = { column: '0', dir: 'asc' }
+ datatable.params[:order]['1'] = { column: '1', dir: 'desc' }
+ expect(datatable.sort_records(records).to_sql).to include(
+ "ORDER BY users.username ASC #{nulls_last_sql(datatable)}, users.email DESC #{nulls_last_sql(datatable)}"
+ )
+ end
+ end
+
+ describe '#sort_records with nulls last using column config' do
+ it 'can handle multiple sorting columns' do
+ skip('unsupported database adapter') if RunningSpec.oracle?
+
+ # set to order by Users username in ascending order, and
+ # by Users email in descending order
+ nulls_last_datatable.params[:order]['0'] = { column: '0', dir: 'asc' }
+ nulls_last_datatable.params[:order]['1'] = { column: '1', dir: 'desc' }
+ expect(nulls_last_datatable.sort_records(records).to_sql).to include(
+ "ORDER BY users.username ASC, users.email DESC #{nulls_last_sql(datatable)}"
+ )
+ end
+ end
+
+end
diff --git a/spec/dummy/app/assets/config/manifest.js b/spec/dummy/app/assets/config/manifest.js
new file mode 100644
index 00000000..e69de29b
diff --git a/spec/dummy/config/database.yml b/spec/dummy/config/database.yml
new file mode 100644
index 00000000..a9de41f6
--- /dev/null
+++ b/spec/dummy/config/database.yml
@@ -0,0 +1,25 @@
+<% adapter = ENV.fetch('http://github.com/DB_ADAPTER', 'postgresql') %>
+test:
+ adapter: <%= adapter %>
+ database: ajax_datatables_rails
+ encoding: utf8
+
+<% if adapter == 'postgresql' || adapter == 'postgis' %>
+ host: '127.0.0.1'
+ port: 5432
+ username: 'postgres'
+ password: 'postgres'
+<% elsif adapter == 'mysql2' || adapter == 'trilogy' %>
+ host: '127.0.0.1'
+ port: 3306
+ username: 'root'
+ password: 'root'
+<% elsif adapter == 'oracle_enhanced' %>
+ host: '127.0.0.1'
+ username: 'oracle_enhanced'
+ password: 'oracle_enhanced'
+ database: 'FREEPDB1'
+<% elsif adapter == 'sqlite3' %>
+ # database: ':memory:'
+ database: db/ajax_datatables_rails.sqlite3
+<% end %>
diff --git a/spec/dummy/config/routes.rb b/spec/dummy/config/routes.rb
new file mode 100644
index 00000000..878c8133
--- /dev/null
+++ b/spec/dummy/config/routes.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+Rails.application.routes.draw do
+ # Add your own routes here, or remove this file if you don't have need for it.
+end
diff --git a/spec/dummy/config/storage.yml b/spec/dummy/config/storage.yml
new file mode 100644
index 00000000..5226545b
--- /dev/null
+++ b/spec/dummy/config/storage.yml
@@ -0,0 +1,3 @@
+test:
+ service: Disk
+ root: /tmp/ajax-datatables-rails/tmp/storage
diff --git a/spec/support/schema.rb b/spec/dummy/db/schema.rb
similarity index 72%
rename from spec/support/schema.rb
rename to spec/dummy/db/schema.rb
index 8da7e4d1..653121e2 100644
--- a/spec/support/schema.rb
+++ b/spec/dummy/db/schema.rb
@@ -1,7 +1,7 @@
-ActiveRecord::Schema.define do
- self.verbose = false
+# frozen_string_literal: true
- create_table :users, :force => true do |t|
+ActiveRecord::Schema.define do
+ create_table :users, force: true do |t|
t.string :username
t.string :email
t.string :first_name
@@ -10,5 +10,4 @@
t.timestamps null: false
end
-
end
diff --git a/spec/dummy/log/.gitignore b/spec/dummy/log/.gitignore
new file mode 100644
index 00000000..397b4a76
--- /dev/null
+++ b/spec/dummy/log/.gitignore
@@ -0,0 +1 @@
+*.log
diff --git a/spec/dummy/public/favicon.ico b/spec/dummy/public/favicon.ico
new file mode 100644
index 00000000..e69de29b
diff --git a/spec/factories/user.rb b/spec/factories/user.rb
index 186adfc2..bcecd98d 100644
--- a/spec/factories/user.rb
+++ b/spec/factories/user.rb
@@ -1,9 +1,11 @@
-FactoryGirl.define do
+# frozen_string_literal: true
+
+FactoryBot.define do
factory :user do |f|
f.username { Faker::Internet.user_name }
f.email { Faker::Internet.email }
f.first_name { Faker::Name.first_name }
f.last_name { Faker::Name.last_name }
- f.post_id { ((1..100).to_a).sample }
+ f.post_id { (1..100).to_a.sample }
end
end
diff --git a/spec/install_oracle.sh b/spec/install_oracle.sh
deleted file mode 100755
index ce02313c..00000000
--- a/spec/install_oracle.sh
+++ /dev/null
@@ -1,12 +0,0 @@
-#!/bin/bash
-
-wget 'https://github.com/cbandy/travis-oracle/archive/v2.0.2.tar.gz'
-mkdir -p ~/.travis/oracle
-tar xz --strip-components 1 -C ~/.travis/oracle -f v2.0.2.tar.gz
-
-~/.travis/oracle/download.sh
-~/.travis/oracle/install.sh
-
-"$ORACLE_HOME/bin/sqlplus" -L -S / AS SYSDBA < (o) { o.upcase } })
- end
-end
diff --git a/spec/support/datatables/complex_datatable.rb b/spec/support/datatables/complex_datatable.rb
new file mode 100644
index 00000000..2999c467
--- /dev/null
+++ b/spec/support/datatables/complex_datatable.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+class ComplexDatatable < AjaxDatatablesRails::ActiveRecord
+ def view_columns
+ @view_columns ||= {
+ username: { source: 'User.username' },
+ email: { source: 'User.email' },
+ first_name: { source: 'User.first_name' },
+ last_name: { source: 'User.last_name' },
+ full_name: { source: 'full_name' },
+ post_id: { source: 'User.post_id', orderable: false },
+ email_hash: { source: 'email_hash', searchable: false },
+ created_at: { source: 'User.created_at' },
+ }
+ end
+
+ def data # rubocop:disable Metrics/MethodLength
+ records.map do |record|
+ {
+ username: record.username,
+ email: record.email,
+ first_name: record.first_name,
+ last_name: record.last_name,
+ full_name: record.full_name,
+ post_id: record.post_id,
+ email_hash: record.email_hash,
+ created_at: record.created_at,
+ }
+ end
+ end
+
+ def get_raw_records # rubocop:disable Naming/AccessorMethodName
+ User.all
+ end
+end
diff --git a/spec/support/datatables/complex_datatable_array.rb b/spec/support/datatables/complex_datatable_array.rb
new file mode 100644
index 00000000..bbcbf03a
--- /dev/null
+++ b/spec/support/datatables/complex_datatable_array.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class ComplexDatatableArray < ComplexDatatable
+ def data
+ records.map do |record|
+ [
+ record.username,
+ record.email,
+ record.first_name,
+ record.last_name,
+ record.post_id,
+ record.created_at,
+ ]
+ end
+ end
+end
diff --git a/spec/support/datatable_cond_date.rb b/spec/support/datatables/datatable_cond_date.rb
similarity index 80%
rename from spec/support/datatable_cond_date.rb
rename to spec/support/datatables/datatable_cond_date.rb
index 65af38c1..510f66b5 100644
--- a/spec/support/datatable_cond_date.rb
+++ b/spec/support/datatables/datatable_cond_date.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class DatatableCondDate < ComplexDatatable
def view_columns
super.deep_merge(created_at: { cond: :date_range })
diff --git a/spec/support/datatable_cond_numeric.rb b/spec/support/datatables/datatable_cond_numeric.rb
similarity index 72%
rename from spec/support/datatable_cond_numeric.rb
rename to spec/support/datatables/datatable_cond_numeric.rb
index 7832bc11..12b016aa 100644
--- a/spec/support/datatable_cond_numeric.rb
+++ b/spec/support/datatables/datatable_cond_numeric.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class DatatableCondEq < ComplexDatatable
def view_columns
super.deep_merge(post_id: { cond: :eq })
@@ -39,3 +41,13 @@ def view_columns
super.deep_merge(post_id: { cond: :in })
end
end
+
+class DatatableCondInWithRegex < DatatableCondIn
+ def view_columns
+ super.deep_merge(post_id: { cond: :in, use_regex: false, orderable: true, formatter: ->(str) { cast_regex_value(str) } })
+ end
+
+ def cast_regex_value(value)
+ value.split('|').map(&:to_i)
+ end
+end
diff --git a/spec/support/datatable_cond_proc.rb b/spec/support/datatables/datatable_cond_proc.rb
similarity index 89%
rename from spec/support/datatable_cond_proc.rb
rename to spec/support/datatables/datatable_cond_proc.rb
index ff057dd5..3823fd12 100644
--- a/spec/support/datatable_cond_proc.rb
+++ b/spec/support/datatables/datatable_cond_proc.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class DatatableCondProc < ComplexDatatable
def view_columns
super.deep_merge(username: { cond: custom_filter })
diff --git a/spec/support/datatables/datatable_cond_string.rb b/spec/support/datatables/datatable_cond_string.rb
new file mode 100644
index 00000000..2cc78c17
--- /dev/null
+++ b/spec/support/datatables/datatable_cond_string.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+class DatatableCondStartWith < ComplexDatatable
+ def view_columns
+ super.deep_merge(first_name: { cond: :start_with })
+ end
+end
+
+class DatatableCondEndWith < ComplexDatatable
+ def view_columns
+ super.deep_merge(last_name: { cond: :end_with })
+ end
+end
+
+class DatatableCondLike < ComplexDatatable
+ def view_columns
+ super.deep_merge(email: { cond: :like })
+ end
+end
+
+class DatatableCondStringEq < ComplexDatatable
+ def view_columns
+ super.deep_merge(email: { cond: :string_eq })
+ end
+end
+
+class DatatableCondStringIn < ComplexDatatable
+ def view_columns
+ super.deep_merge(email: { cond: :string_in, formatter: ->(o) { o.split('|') } })
+ end
+end
+
+class DatatableCondNullValue < ComplexDatatable
+ def view_columns
+ super.deep_merge(email: { cond: :null_value })
+ end
+end
+
+class DatatableWithFormater < ComplexDatatable
+ def view_columns
+ super.deep_merge(last_name: { formatter: lambda(&:upcase) })
+ end
+end
diff --git a/spec/support/datatables/datatable_cond_unknown.rb b/spec/support/datatables/datatable_cond_unknown.rb
new file mode 100644
index 00000000..c730b575
--- /dev/null
+++ b/spec/support/datatables/datatable_cond_unknown.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class DatatableCondUnknown < ComplexDatatable
+ def view_columns
+ super.deep_merge(username: { cond: :foo })
+ end
+end
diff --git a/spec/support/datatables/datatable_custom_column.rb b/spec/support/datatables/datatable_custom_column.rb
new file mode 100644
index 00000000..2d8db393
--- /dev/null
+++ b/spec/support/datatables/datatable_custom_column.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class DatatableCustomColumn < ComplexDatatable
+ def view_columns
+ super.deep_merge(full_name: { cond: filter_full_name })
+ end
+
+ def get_raw_records # rubocop:disable Naming/AccessorMethodName
+ User.select("*, CONCAT(first_name, ' ', last_name) as full_name")
+ end
+
+ private
+
+ def filter_full_name
+ ->(_column, value) { ::Arel::Nodes::SqlLiteral.new("CONCAT(first_name, ' ', last_name)").matches("#{value}%") }
+ end
+end
diff --git a/spec/support/datatables/datatable_order_nulls_last.rb b/spec/support/datatables/datatable_order_nulls_last.rb
new file mode 100644
index 00000000..e1b3acdc
--- /dev/null
+++ b/spec/support/datatables/datatable_order_nulls_last.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class DatatableOrderNullsLast < ComplexDatatable
+ def view_columns
+ super.deep_merge(email: { nulls_last: true })
+ end
+end
diff --git a/spec/support/datatables/grouped_datatable_array.rb b/spec/support/datatables/grouped_datatable_array.rb
new file mode 100644
index 00000000..e23e0126
--- /dev/null
+++ b/spec/support/datatables/grouped_datatable_array.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+class GroupedDatatable < ComplexDatatable
+
+ def get_raw_records # rubocop:disable Naming/AccessorMethodName
+ User.all.group(:id)
+ end
+end
diff --git a/spec/support/test_helpers.rb b/spec/support/helpers/params.rb
similarity index 54%
rename from spec/support/test_helpers.rb
rename to spec/support/helpers/params.rb
index 97201b54..0323ab51 100644
--- a/spec/support/test_helpers.rb
+++ b/spec/support/helpers/params.rb
@@ -1,4 +1,6 @@
-# rubocop:disable Metrics/MethodLength
+# frozen_string_literal: true
+
+# rubocop:disable Metrics/MethodLength, Layout/HashAlignment
def sample_params
ActionController::Parameters.new(
{
@@ -29,12 +31,24 @@ def sample_params
}
},
'4' => {
- 'data' => 'post_id', 'name' => '', 'searchable' => 'true', 'orderable' => 'true',
+ 'data' => 'full_name', 'name' => '', 'searchable' => 'true', 'orderable' => 'true',
'search' => {
'value' => '', 'regex' => 'false'
}
},
'5' => {
+ 'data' => 'post_id', 'name' => '', 'searchable' => 'true', 'orderable' => 'true',
+ 'search' => {
+ 'value' => '', 'regex' => 'false'
+ }
+ },
+ '6' => {
+ 'data' => 'email_hash', 'name' => '', 'searchable' => 'false', 'orderable' => 'true',
+ 'search' => {
+ 'value' => '', 'regex' => 'false'
+ }
+ },
+ '7' => {
'data' => 'created_at', 'name' => '', 'searchable' => 'true', 'orderable' => 'true',
'search' => {
'value' => '', 'regex' => 'false'
@@ -42,58 +56,33 @@ def sample_params
},
},
'order' => {
- '0' => {'column' => '0', 'dir' => 'asc'}
+ '0' => { 'column' => '0', 'dir' => 'asc' },
},
- 'start' => '0', 'length' => '10', 'search' => {
+ 'start' => '0',
+ 'length' => '10',
+ 'search' => {
'value' => '', 'regex' => 'false'
},
- '_' => '1423364387185'
+ '_' => '1423364387185',
}
)
end
-# rubocop:enable Metrics/MethodLength
-
-class ComplexDatatable < AjaxDatatablesRails::Base
- def view_columns
- @view_columns ||= {
- username: { source: 'User.username' },
- email: { source: 'User.email' },
- first_name: { source: 'User.first_name' },
- last_name: { source: 'User.last_name' },
- post_id: { source: 'User.post_id' },
- created_at: { source: 'User.created_at' },
- }
- end
-
- def data
- records.map do |record|
- {
- username: record.username,
- email: record.email,
- first_name: record.first_name,
- last_name: record.last_name,
- post_id: record.post_id,
- created_at: record.created_at,
- }
- end
- end
+# rubocop:enable Metrics/MethodLength, Layout/HashAlignment
- def get_raw_records
- User.all
- end
+def sample_params_json
+ hash_params = sample_params.to_unsafe_h
+ hash_params['columns'] = hash_params['columns'].values
+ hash_params['order'] = hash_params['order'].values
+ ActionController::Parameters.new(hash_params)
end
-class ComplexDatatableArray < ComplexDatatable
- def data
- records.map do |record|
- [
- record.username,
- record.email,
- record.first_name,
- record.last_name,
- record.post_id,
- record.created_at,
- ]
- end
+def nulls_last_sql(datatable)
+ case datatable.db_adapter
+ when :pg, :postgresql, :postgres, :oracle, :postgis
+ 'NULLS LAST'
+ when :mysql, :mysql2, :trilogy, :sqlite, :sqlite3
+ 'IS NULL'
+ else
+ raise 'unsupported database adapter'
end
end
diff --git a/spec/support/models/user.rb b/spec/support/models/user.rb
new file mode 100644
index 00000000..34d527e2
--- /dev/null
+++ b/spec/support/models/user.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'digest'
+
+class User < ActiveRecord::Base
+ def full_name
+ "#{first_name} #{last_name}"
+ end
+
+ def email_hash
+ return nil if email.nil?
+
+ Digest::SHA256.hexdigest email
+ end
+end
diff --git a/spec/support/test_models.rb b/spec/support/test_models.rb
deleted file mode 100644
index 4a57cf07..00000000
--- a/spec/support/test_models.rb
+++ /dev/null
@@ -1,2 +0,0 @@
-class User < ActiveRecord::Base
-end
|