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..1b22f5e2 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,151 @@ +--- +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 + 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 + + 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 + - oracle_enhanced + - 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' + + # Disabled for now because of build error: + # /opt/hostedtoolcache/Ruby/3.0.7/x64/lib/ruby/3.0.0/psych.rb:457:in + # `parse_stream': undefined method `parse' for #>, + # @external_encoding=0> (NoMethodError) + # from + # /home/runner/work/ajax-datatables-rails/ajax-datatables-rails/vendor/bundle/ruby/3.0.0/gems/ruby-oci8-2.2.14/ext/oci8/apiwrap.rb:64:in + # `create_apiwrap' + - rails: 'rails_7.2' + adapter: 'oracle_enhanced' + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set DB Adapter + env: + DB_ADAPTER: ${{ matrix.adapter }} + CUSTOM_ORACLE_FILE: ${{ secrets.CUSTOM_ORACLE_FILE }} + + # 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 + + if [[ "${DB_ADAPTER}" == "oracle_enhanced" ]]; then + ./spec/install_oracle.sh + # Fix error : libnnz11.so: cannot open shared object file: No such file or directory + sudo ln -s ${ORACLE_HOME}/lib/libnnz11.so /usr/lib/libnnz11.so + fi + + - name: Setup Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + bundler-cache: true + env: + DB_ADAPTER: ${{ matrix.adapter }} + + - name: RSpec & publish code coverage + uses: paambaati/codeclimate-action@v9.0.0 + env: + DB_ADAPTER: ${{ matrix.adapter }} + CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} + with: + coverageCommand: bin/rspec 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/.rspec b/.rspec index 4e1e0d2f..372b5acf 100644 --- a/.rspec +++ b/.rspec @@ -1 +1 @@ ---color +--warnings diff --git a/.rubocop.yml b/.rubocop.yml index 5786d29b..3f8009ae 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,19 +1,49 @@ +--- +require: + - rubocop-factory_bot + - rubocop-performance + - rubocop-rake + - rubocop-rspec + AllCops: - TargetRubyVersion: 2.5 + NewCops: enable + TargetRubyVersion: 3.1 Exclude: - - spec/**/*.rb - - lib/ajax-datatables-rails.rb - - lib/generators/rails/templates/*.rb + - bin/* + - gemfiles/* + - spec/dummy/**/* -Documentation: - Enabled: false +######### +# STYLE # +######### -Gemspec/OrderedDependencies: +Style/Documentation: Enabled: false +Style/TrailingCommaInArrayLiteral: + EnforcedStyleForMultiline: comma + +Style/TrailingCommaInHashLiteral: + EnforcedStyleForMultiline: comma + +Style/BlockDelimiters: + AllowedPatterns: ['expect'] + +########## +# LAYOUT # +########## + +Layout/LineLength: + Max: 150 + Exclude: + - ajax-datatables-rails.gemspec + Layout/EmptyLines: Enabled: false +Layout/EmptyLineBetweenDefs: + Enabled: false + Layout/EmptyLinesAroundClassBody: Enabled: false @@ -23,26 +53,33 @@ Layout/EmptyLinesAroundBlockBody: Layout/EmptyLinesAroundModuleBody: Enabled: false -Layout/EmptyLineBetweenDefs: - Enabled: false +Layout/HashAlignment: + EnforcedColonStyle: table + EnforcedHashRocketStyle: table -Metrics/CyclomaticComplexity: - Max: 7 +########## +# NAMING # +########## -Metrics/LineLength: - Enabled: false +Naming/FileName: + Exclude: + - lib/ajax-datatables-rails.rb -Metrics/BlockLength: - Max: 30 +######### +# RSPEC # +######### -Metrics/MethodLength: - Max: 15 +RSpec/MultipleExpectations: + Max: 7 -Metrics/ClassLength: - Max: 130 +RSpec/NestedGroups: + Max: 6 -Naming/AccessorMethodName: - Enabled: false +RSpec/ExampleLength: + Max: 9 -Style/NumericPredicate: - Enabled: false +RSpec/MultipleMemoizedHelpers: + Max: 6 + +RSpec/NotToNot: + EnforcedStyle: to_not diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index b05d792d..00000000 --- a/.travis.yml +++ /dev/null @@ -1,80 +0,0 @@ -dist: trusty -language: ruby -sudo: required -cache: bundler -rvm: - - 2.2.10 - - 2.3.7 -gemfile: - - gemfiles/rails_4.0.13.gemfile - - gemfiles/rails_4.1.16.gemfile - - gemfiles/rails_4.2.10.gemfile - - gemfiles/rails_5.0.7.gemfile - - gemfiles/rails_5.1.6.gemfile - - gemfiles/rails_5.2.0.gemfile -matrix: - include: - - rvm: 2.4.4 - gemfile: gemfiles/rails_4.2.10.gemfile - env: DB_ADAPTER=postgresql - - rvm: 2.4.4 - gemfile: gemfiles/rails_5.0.7.gemfile - env: DB_ADAPTER=postgresql - - rvm: 2.4.4 - gemfile: gemfiles/rails_5.1.6.gemfile - env: DB_ADAPTER=postgresql - - rvm: 2.4.4 - gemfile: gemfiles/rails_5.2.0.gemfile - env: DB_ADAPTER=postgresql - - rvm: 2.4.4 - gemfile: gemfiles/rails_4.2.10.gemfile - env: DB_ADAPTER=mysql2 - - rvm: 2.4.4 - gemfile: gemfiles/rails_5.0.7.gemfile - env: DB_ADAPTER=mysql2 - - rvm: 2.4.4 - gemfile: gemfiles/rails_5.1.6.gemfile - env: DB_ADAPTER=mysql2 - - rvm: 2.4.4 - gemfile: gemfiles/rails_5.2.0.gemfile - env: DB_ADAPTER=mysql2 - - rvm: 2.4.4 - gemfile: gemfiles/rails_4.2.10.gemfile - env: DB_ADAPTER=oracle_enhanced - - rvm: 2.4.4 - gemfile: gemfiles/rails_5.0.7.gemfile - env: DB_ADAPTER=oracle_enhanced - - rvm: 2.4.4 - gemfile: gemfiles/rails_5.1.6.gemfile - env: DB_ADAPTER=oracle_enhanced - - rvm: 2.4.4 - gemfile: gemfiles/rails_5.2.0.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: - - gem update --system - - gem install bundler - - 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 de10f668..dc6bea81 100644 --- a/Appraisals +++ b/Appraisals @@ -1,40 +1,118 @@ # frozen_string_literal: true -RAILS_VERSIONS = { - '4.0.13' => { - 'mysql2' => '~> 0.3.18', - 'activerecord-oracle_enhanced-adapter' => '~> 1.5.0' - }, - '4.1.16' => { - 'mysql2' => '~> 0.3.18', - 'activerecord-oracle_enhanced-adapter' => '~> 1.5.0' - }, - '4.2.10' => { - 'activerecord-oracle_enhanced-adapter' => '~> 1.6.0' - }, - '5.0.7' => { - 'activerecord-oracle_enhanced-adapter' => '~> 1.7.0', - 'ruby-oci8' => '' - }, - '5.1.6' => { - 'activerecord-oracle_enhanced-adapter' => '~> 1.8.0', - 'ruby-oci8' => '' - }, - '5.2.0' => { - 'activerecord-oracle_enhanced-adapter' => '~> 5.2.0', - 'ruby-oci8' => '' - } -}.freeze - -RAILS_VERSIONS.each do |version, gems| - appraise "rails_#{version}" do - gem 'rails', version - gems.each do |name, gem_version| - if gem_version.empty? - gem name - else - gem name, gem_version - end - end - end +############### +# 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' +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', git: 'https://github.com/rsim/oracle-enhanced.git' + remove_gem 'pg' +end + +appraise 'rails_7.2_with_postgis' do + gem 'rails', '~> 7.2.0' + gem 'pg' + gem 'activerecord-postgis-adapter', git: 'https://github.com/rgeo/activerecord-postgis-adapter.git' +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', git: 'https://github.com/rsim/oracle-enhanced.git' + remove_gem 'pg' +end + +appraise 'rails_8.0_with_postgis' do + gem 'rails', '~> 8.0.0' + gem 'pg' + gem 'activerecord-postgis-adapter', git: 'https://github.com/rgeo/activerecord-postgis-adapter.git' end diff --git a/CHANGELOG.md b/CHANGELOG.md index 8cc97a6b..10407924 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,107 @@ # CHANGELOG +## 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) @@ -12,9 +114,6 @@ * Change: Add # frozen_string_literal: true pragma * Various improvements in internal API -**Note :** This is the last version to support Rails 4.0.x and Rails 4.1.x - - ## 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). diff --git a/Gemfile b/Gemfile index 4644180e..47a46474 100644 --- a/Gemfile +++ b/Gemfile @@ -4,7 +4,26 @@ 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-retry' +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 64e354a0..62fd2b29 100644 --- a/README.md +++ b/README.md @@ -3,21 +3,18 @@ [![GitHub license](https://img.shields.io/github/license/jbox-web/ajax-datatables-rails.svg)](https://github.com/jbox-web/ajax-datatables-rails/blob/master/LICENSE) [![Gem](https://img.shields.io/gem/v/ajax-datatables-rails.svg)](https://rubygems.org/gems/ajax-datatables-rails) [![Gem](https://img.shields.io/gem/dtv/ajax-datatables-rails.svg)](https://rubygems.org/gems/ajax-datatables-rails) -[![Build Status](https://travis-ci.org/jbox-web/ajax-datatables-rails.svg?branch=master)](https://travis-ci.org/jbox-web/ajax-datatables-rails) +[![CI](https://github.com/jbox-web/ajax-datatables-rails/workflows/CI/badge.svg)](https://github.com/jbox-web/ajax-datatables-rails/actions) [![Code Climate](https://codeclimate.com/github/jbox-web/ajax-datatables-rails/badges/gpa.svg)](https://codeclimate.com/github/jbox-web/ajax-datatables-rails) [![Test Coverage](https://codeclimate.com/github/jbox-web/ajax-datatables-rails/badges/coverage.svg)](https://codeclimate.com/github/jbox-web/ajax-datatables-rails/coverage) -[![Dependency Status](https://gemnasium.com/jbox-web/ajax-datatables-rails.svg)](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.16 / 4.2.10 / 5.0.7 / 5.1.6 / 5.2.0 -* Ruby 2.2.10 / 2.3.7 / 2.4.4 / 2.5.1 -* Postgresql 9.6 -* MySQL 5.6 -* Oracle XE 11.2 (thanks to [travis-oracle](https://github.com/cbandy/travis-oracle)) -* SQLite3 +* 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 ## Description @@ -39,22 +36,9 @@ The final goal of this gem is to **generate a JSON** content that will be given 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/). +You'll find a sample project here : https://ajax-datatables-rails.herokuapp.com -## Warning - -**Breaking changes :** the *v0.4* version is a **major break** from *v0.3*. - -The core has been rewriten to remove dependency on [Kaminari](https://github.com/kaminari/kaminari) or [WillPaginate](https://github.com/mislav/will_paginate). - -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](#3-customize-the-generated-datatables-class) for more infos. - -To migrate on the v0.4 you'll need to : - -* 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](#5-wire-up-the-javascript)) +Its real world examples. The code is here : https://github.com/jbox-web/ajax-datatables-rails-sample-project ## Installation @@ -75,39 +59,12 @@ We assume here that you have already installed [jQuery DataTables](https://datat You can install jQuery DataTables : -* with the [`jquery-datatables-rails`](https://github.com/rweng/jquery-datatables-rails) gem (which is a bit outdated) +* 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) -## Configuration - -Generate the `ajax-datatables-rails` config file with this command : - -```sh -$ bundle exec rails generate datatable:config -``` - -Doing so, will create the `config/initializers/ajax_datatables_rails.rb` file with the following content : - -```ruby -AjaxDatatablesRails.configure do |config| - # available options for db_adapter are: :pg, :mysql, :mysql2, :sqlite, :sqlite3 - # config.db_adapter = :pg - - # Or you can use your rails environment adapter if you want a generic dev and production - # config.db_adapter = Rails.configuration.database_configuration[Rails.env]['adapter'].to_sym - - # available options for orm are: :active_record, :mongoid - # config.orm = :active_record -end -``` - -Uncomment the `config.db_adapter` line and set the corresponding value to your database and gem. This is all you need. - -Uncomment the `config.orm` line to set `active_record or mongoid` if included in your project. It defaults to `active_record`. - -#### Note +## Note Currently `AjaxDatatablesRails` only supports `ActiveRecord` as ORM for performing database queries. @@ -119,7 +76,7 @@ If you'd be interested in contributing to speed development, please [open an iss ## 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, just be sure to have [configured the right adapter](#configuration)) +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. @@ -194,7 +151,7 @@ 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" }, } @@ -205,14 +162,40 @@ 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, see [here](#daterange-search)) +* `: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) + +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 + + # ... 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 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 : @@ -226,12 +209,13 @@ def data 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 : + +**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. @@ -291,7 +275,7 @@ def additional_data 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. ### 4) Setup the Controller action @@ -302,7 +286,7 @@ 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 ``` @@ -311,6 +295,8 @@ Don't forget to make sure the proper route has been added to `config/routes.rb`. [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). + ### 5) Wire up the Javascript Finally, the javascript to tie this all together. In the appropriate `coffee` file: @@ -322,7 +308,8 @@ $ -> $('#users-datatable').dataTable processing: true serverSide: true - ajax: $('#users-datatable').data('source') + ajax: + url: $('#users-datatable').data('source') pagingType: 'full_numbers' columns: [ {data: 'id'} @@ -345,7 +332,9 @@ jQuery(document).ready(function() { $('#users-datatable').dataTable({ "processing": true, "serverSide": true, - "ajax": $('#users-datatable').data('source'), + "ajax": { + "url": $('#users-datatable').data('source') + }, "pagingType": "full_numbers", "columns": [ {"data": "id"}, @@ -371,7 +360,9 @@ Sometimes you'll need to use view helper methods like `link_to`, `mail_to`, To have these methods available to be used, this is the way to go: ```ruby -class MyCustomDatatable < AjaxDatatablesRails::Base +class UserDatatable < AjaxDatatablesRails::ActiveRecord + extend Forwardable + # either define them one-by-one def_delegator :@view, :check_box_tag def_delegator :@view, :link_to @@ -383,6 +374,11 @@ class MyCustomDatatable < AjaxDatatablesRails::Base # ... other methods (view_columns, get_raw_records...) + def initialize(params, opts = {}) + @view = opts[:view_context] + super + end + # now, you'll have these methods available to be used anywhere def data records.map do |record| @@ -397,15 +393,29 @@ class MyCustomDatatable < AjaxDatatablesRails::Base end end end + +# 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 ``` +### 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. +**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). + Example : ```ruby -... +class UserDatatable < AjaxDatatablesRails::ActiveRecord + ... def data records.map do |record| { @@ -418,7 +428,8 @@ Example : } end end -... + ... +end class UserDecorator < ApplicationDecorator delegate :last_name, :bio @@ -447,16 +458,9 @@ class UserDecorator < ApplicationDecorator end ``` -**Note :** On the long term it's much more cleaner than using `def_delegator` since decorators are reusable everywhere in your application :) - -So we **strongly recommand you to use Draper decorators.** It will help keeping your DataTables class small and clean and keep focused on what they should do (mostly) : filtering records ;) - -**Note 2 :** The `def_delegator` might disappear in a near future : [#288 [RFC] Remove dependency on view_context](https://github.com/jbox-web/ajax-datatables-rails/issues/288). -You're invited to give your opinion :) - ### Pass options to the datatable class -An `AjaxDatatablesRails::Base` inherited class can accept an options hash at initialization. This provides room for flexibility when required. +An `AjaxDatatablesRails::ActiveRecord` inherited class can accept an options hash at initialization. This provides room for flexibility when required. Example: @@ -465,12 +469,12 @@ Example: def index respond_to do |format| format.html - format.json { render json: UserDatatable.new(view_context, user: current_user, from: 1.month.ago) } + format.json { render json: UserDatatable.new(params, user: current_user, from: 1.month.ago) } end end # The datatable class -class UnrespondedMessagesDatatable < AjaxDatatablesRails::Base +class UnrespondedMessagesDatatable < AjaxDatatablesRails::ActiveRecord # ... other methods (view_columns, data...) @@ -495,6 +499,24 @@ class UnrespondedMessagesDatatable < AjaxDatatablesRails::Base 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 MySharedModelDatatable < AjaxDatatablesRails::ActiveRecord + self.db_adapter = :oracle_enhanced + + # ... other methods (view_columns, data...) + + def get_raw_records + AnimalsRecord.connected_to(role: :reading) do + Dog.all + end + end +end +``` + ### Columns syntax You can mix several model in the same datatable. @@ -508,13 +530,13 @@ available in our datatable to search and sort by. def view_columns @view_columns ||= { - first_name: 'User.first_name', - last_name: 'User.last_name', - order_number: 'PurchaseOrder.number', - order_created_at: 'PurchaseOrder.created_at', - quantity: 'Purchase::LineItem.quantity', - unit_price: 'Purchase::LineItem.unit_price', - item_total: 'Purchase::LineItem.item_total' + 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 ``` @@ -546,14 +568,14 @@ The related definition would be : ```ruby def view_columns @view_columns ||= { - course_type: 'CourseType.name', - course_name: 'Course.name', - contact_name: 'Contact.full_name', - competency_type: 'CompetencyType.name', - event_title: 'Event.title', - event_start: 'Event.event_start', - event_end: 'Event.event_end', - event_status: 'Event.status', + 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 @@ -610,18 +632,18 @@ See [DefaultScope is evil](https://rails-bestpractices.com/posts/2013/06/15/defa ### DateRange search -This feature works with [yadcf](https://github.com/vedmack/yadcf). +This feature works with [datatables-factory](https://github.com/jbox-web/datatables-factory) (or [yadcf](https://github.com/vedmack/yadcf)). To enable the date range search, for example `created_at` : -* add a 'created_at' `` in your html +* 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::Base` is not tied to an +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: @@ -641,7 +663,123 @@ 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. -### Creating indices for Postgresql +## Tests + +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)) + +### Use HTTP `POST` method (Medium) + +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). + +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). @@ -668,27 +806,13 @@ def change end ``` -### Speedup JSON rendering - -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 -You'll find a sample project [here](https://github.com/ajahongir/ajax-datatables-rails-v-0-4-0-how-to). Its real world example. +Filtering by JSONB column values : [#277](https://github.com/jbox-web/ajax-datatables-rails/issues/277#issuecomment-366526373) + +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) -Filtering by JSONB column values : [#277](https://github.com/jbox-web/ajax-datatables-rails/issues/277) +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 c0d0eb86..ad26ae7d 100644 --- a/Rakefile +++ b/Rakefile @@ -5,11 +5,3 @@ 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 780b46b7..69e22ea8 100644 --- a/ajax-datatables-rails.gemspec +++ b/ajax-datatables-rails.gemspec @@ -1,12 +1,10 @@ # frozen_string_literal: true -# coding: utf-8 -$LOAD_PATH.push File.expand_path('../lib', __FILE__) -require 'ajax-datatables-rails/version' +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'] @@ -14,26 +12,18 @@ Gem::Specification.new do |s| 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', '< 1.0' - 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_bot' - 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/config.ru b/config.ru new file mode 100644 index 00000000..8e5bee22 --- /dev/null +++ b/config.ru @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +Bundler.require :default, :development + +Combustion.path = 'spec/dummy' +Combustion.initialize! :all +run Combustion::Application diff --git a/doc/migrate.md b/doc/migrate.md new file mode 100644 index 00000000..685913bb --- /dev/null +++ b/doc/migrate.md @@ -0,0 +1,97 @@ +## To migrate from `v1.x` to `v1.3.0` + +The *v1.3.0* version has some breaking changes : + +* `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)) + +This change is transparent for everyone. Just remove `AjaxDatatablesRails.config.db_adapter=` from your configuration (if exists) and it should work fine. + +Now you can use AjaxDatatablesRails in multi-db environments. + +* `AjaxDatatablesRails.config.nulls_last=` is removed and is configured per datatable class now (or by column). It defaults to false. + +This change is easy to mitigate : add `self.nulls_last = true` in [`ApplicationDatatable`](https://github.com/jbox-web/ajax-datatables-rails#create-a-master-parent-class-easy) and remove `AjaxDatatablesRails.config.nulls_last=` + +```ruby +class ApplicationDatatable < AjaxDatatablesRails::ActiveRecord + self.nulls_last = true + # puts commonly used methods here +end +``` + +* `AjaxDatatablesRails.config` is removed with no replacement + +Fix the two changes above and remove any configuration file about AjaxDatatablesRails. The gem is now configless :) + +## To migrate from `v0.4.x` to `v1.0.0` + +The *v1.0.0* version is a **major break** from *v0.4*. + +* Datatables no longer inherits from `AjaxDatatablesRails::Base` but from `AjaxDatatablesRails::ActiveRecord` (this solves [#228](https://github.com/jbox-web/ajax-datatables-rails/issues/228)) +* The `view_context` is no longer injected in Datatables but only the `params` hash (see the [example](#4-setup-the-controller-action)). This will break calls to helpers methods. + +1) To mitigate the first change (Datatables no longer inherits from `AjaxDatatablesRails::Base` but from `AjaxDatatablesRails::ActiveRecord`) + +Create a new `ApplicationDatatable` class and make all your classes inherits from it : + +```ruby +class ApplicationDatatable < AjaxDatatablesRails::ActiveRecord +end + +class PostDatatable < ApplicationDatatable +end +``` + +**Note :** This is now in the [ProTips™](https://github.com/jbox-web/ajax-datatables-rails#protips) section of the documentation. + +2) To mitigate the second change (The `view_context` is no longer injected in Datatables) + +Update the `ApplicationDatatable` class : + +```ruby +class ApplicationDatatable < AjaxDatatablesRails::ActiveRecord + extend Forwardable + attr_reader :view + def initialize(params, opts = {}) + @view = opts[:view_context] + super + end +end +``` + +and update your controllers : + +```ruby +# before +respond_to do |format| + format.json { render json: UserDatatable.new(view_context) } +end + +# after +respond_to do |format| + format.json { render json: UserDatatable.new(params, view_context: view_context) } +end + +# if you need to inject some options +respond_to do |format| + format.json { render json: UserDatatable.new(params, view_context: view_context, my: 'options') } +end +``` + +This way, you can still use `def_delegators` in your datatables [as in the documentation](https://github.com/jbox-web/ajax-datatables-rails#using-view-helpers). + +Note that the recommanded way is to use [Draper gem](https://github.com/drapergem/draper) to separate filtering logic from view/presentation logic [as in the documentation](https://github.com/jbox-web/ajax-datatables-rails#using-view-decorators). + +## To migrate from `v0.3.x` to `v0.4.x` + +The *v0.4* version is a **major break** from *v0.3*. + +The core has been rewriten to remove dependency on [Kaminari](https://github.com/kaminari/kaminari) or [WillPaginate](https://github.com/mislav/will_paginate). + +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. + +To migrate on the v0.4 you'll need to : + +* 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](https://github.com/jbox-web/ajax-datatables-rails#5-wire-up-the-javascript)) diff --git a/doc/webpack.md b/doc/webpack.md index e0d001e0..70239a0c 100644 --- a/doc/webpack.md +++ b/doc/webpack.md @@ -5,7 +5,9 @@ We assume here that Bootstrap and FontAwesome are already installed with Webpack Inspired by https://datatables.net/download and completed : Add npm packages : - +```sh +$ yarn add imports-loader +``` ```sh $ yarn add datatables.net $ yarn add datatables.net-bs @@ -22,7 +24,10 @@ In `config/webpack/loaders/datatables.js` : ```js module.exports = { test: /datatables\.net.*/, - loader: 'imports-loader?define=>false' + loader: 'imports-loader', + options: { + additionalCode: 'var define = false;' + } } ``` diff --git a/gemfiles/rails_4.0.13.gemfile b/gemfiles/rails_4.0.13.gemfile deleted file mode 100644 index 8e751c3c..00000000 --- a/gemfiles/rails_4.0.13.gemfile +++ /dev/null @@ -1,14 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "rails", "4.0.13" -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.1.16.gemfile b/gemfiles/rails_4.1.16.gemfile deleted file mode 100644 index 8af0b017..00000000 --- a/gemfiles/rails_4.1.16.gemfile +++ /dev/null @@ -1,14 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "rails", "4.1.16" -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.10.gemfile b/gemfiles/rails_4.2.10.gemfile deleted file mode 100644 index e4d87c7b..00000000 --- a/gemfiles/rails_4.2.10.gemfile +++ /dev/null @@ -1,14 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "rails", "4.2.10" -gem "mysql2", "0.4.10" -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.7.gemfile b/gemfiles/rails_5.0.7.gemfile deleted file mode 100644 index e31d7c24..00000000 --- a/gemfiles/rails_5.0.7.gemfile +++ /dev/null @@ -1,13 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "rails", "5.0.7" -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.6.gemfile b/gemfiles/rails_5.1.6.gemfile deleted file mode 100644 index 25ae1251..00000000 --- a/gemfiles/rails_5.1.6.gemfile +++ /dev/null @@ -1,13 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "rails", "5.1.6" -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_5.2.0.gemfile b/gemfiles/rails_5.2.0.gemfile deleted file mode 100644 index f3b915c7..00000000 --- a/gemfiles/rails_5.2.0.gemfile +++ /dev/null @@ -1,13 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "rails", "5.2.0" -gem "activerecord-oracle_enhanced-adapter", "~> 5.2.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..8be9ba3b --- /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-retry" +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..01410313 --- /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-retry" +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..ffaad7df --- /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-retry" +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" + +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..042ce7ac --- /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-retry" +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..d23c1582 --- /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-retry" +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..2500f361 --- /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-retry" +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..3d8cc0ed --- /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-retry" +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..17280e0e --- /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-retry" +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", git: "https://github.com/rsim/oracle-enhanced.git" + +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..7f0a4e17 --- /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-retry" +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", git: "https://github.com/rgeo/activerecord-postgis-adapter.git" + +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..ee0c0c1a --- /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-retry" +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..8ce650f8 --- /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-retry" +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..fcdf669e --- /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-retry" +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..023c71d8 --- /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-retry" +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..563ac16c --- /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-retry" +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", git: "https://github.com/rsim/oracle-enhanced.git" + +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..c2498b58 --- /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-retry" +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", git: "https://github.com/rgeo/activerecord-postgis-adapter.git" + +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..164f2e09 --- /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-retry" +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..62bfdd83 --- /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-retry" +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..f80123d1 --- /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-retry" +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 9675176c..cb9f515f 100644 --- a/lib/ajax-datatables-rails.rb +++ b/lib/ajax-datatables-rails.rb @@ -1,3 +1,17 @@ # frozen_string_literal: true -require 'ajax_datatables_rails' +# 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 +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 71cb674a..4d52feee 100644 --- a/lib/ajax-datatables-rails/base.rb +++ b/lib/ajax-datatables-rails/base.rb @@ -1,93 +1,119 @@ # 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 + + class_attribute :db_adapter, default: default_db_adapter + class_attribute :nulls_last, default: false + + attr_reader :params, :options, :datatable GLOBAL_SEARCH_DELIMITER = ' ' - def initialize(view, options = {}) - @view = view - @options = options - load_orm_extension - end + 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 - raise(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 - raise(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 - raise(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 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(get_additional_data) + 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 - end - - private - - # This method is necessary for smooth transition from - # `additinonal_datas` method to `additional_data` - # without breaking change. - def get_additional_data - if respond_to?(:additional_datas) - puts <<-WARNING - `additional_datas` has been deprecated and - will be removed in next major version update! - Please use `additional_data` instead. - WARNING - - additional_datas - else - additional_data - end + @search_columns ||= searchable_columns.select(&:searched?) end - def sanitize(data) + def sanitize_data(data) data.map do |record| if record.is_a?(Array) record.map { |td| ERB::Util.html_escape(td) } @@ -97,6 +123,11 @@ def sanitize(data) end end + # called from within #data + def records + @records ||= retrieve_records + end + def retrieve_records records = fetch_records records = filter_records(records) @@ -106,52 +137,24 @@ def retrieve_records end def records_total_count - fetch_records.count(:all) + numeric_count fetch_records.count(:all) end def records_filtered_count - filter_records(fetch_records).count(:all) + numeric_count filter_records(fetch_records).count(:all) end - # Private helper methods - def load_orm_extension - case AjaxDatatablesRails.config.orm - when :active_record - extend ORM::ActiveRecord - when :mongoid - 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 - <<-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_error_text - <<-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 - - def view_columns_error_text - <<-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 + # See: https://datatables.net/manual/server-side#Returned-data + def draw_id + params[:draw].present? ? { draw: params[:draw].to_i } : {} end end diff --git a/lib/ajax-datatables-rails/config.rb b/lib/ajax-datatables-rails/config.rb deleted file mode 100644 index bbcb3c4d..00000000 --- a/lib/ajax-datatables-rails/config.rb +++ /dev/null @@ -1,31 +0,0 @@ -# frozen_string_literal: true - -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 } - config_accessor(:nulls_last) { false } - 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 81ce4b77..2179a4da 100644 --- a/lib/ajax-datatables-rails/datatable/column.rb +++ b/lib/ajax-datatables-rails/datatable/column.rb @@ -4,28 +4,30 @@ module AjaxDatatablesRails module Datatable class Column - DB_ADAPTER_TYPE_CAST = { - mysql: 'CHAR', - mysql2: 'CHAR', - sqlite: 'TEXT', - sqlite3: 'TEXT', - oracle: 'VARCHAR2(4000)', - oracleenhanced: 'VARCHAR2(4000)' - }.freeze - - attr_reader :datatable, :index, :options - attr_writer :search - include Search include Order - prepend DateFilter unless AjaxDatatablesRails.old_rails? + include DateFilter + attr_reader :datatable, :index, :options, :column_name + attr_writer :search - def initialize(datatable, index, options) + def initialize(datatable, index, options) # rubocop:disable Metrics/MethodLength @datatable = datatable @index = index @options = options - @view_column = datatable.view_columns[options[:data].to_sym] + @column_name = options[:data]&.to_sym + @view_column = datatable.view_columns[@column_name] + + @model = nil + @field = nil + @type_cast = nil + @casted_column = nil + @search = nil + @delimiter = nil + @range_start = nil + @range_end = nil + + validate_settings! end def data @@ -41,37 +43,91 @@ def table end def model - @model ||= source.split('.').first.constantize + @model ||= custom_field? ? source : source.split('.').first.constantize end def field - @field ||= source.split('.').last.to_sym + @field ||= custom_field? ? source : source.split('.').last.to_sym end def custom_field? !source.include?('.') end - # Add formater option to allow modification of the value + # Add formatter option to allow modification of the value # before passing it to the database - def formater - @view_column[:formater] + 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 + 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[AjaxDatatablesRails.config.db_adapter] || 'VARCHAR') + @type_cast ||= DB_ADAPTER_TYPE_CAST.fetch(datatable.db_adapter, TYPE_CAST_DEFAULT) end def casted_column @casted_column ||= ::Arel::Nodes::NamedFunction.new('CAST', [table[field].as(type_cast)]) 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 valid_search_column?(column_name) + !datatable.view_columns[column_name].nil? + 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 + + private_constant :VALID_SEARCH_CONDITIONS + + def valid_search_condition?(cond) + return true if cond.is_a?(Proc) + + VALID_SEARCH_CONDITIONS.include?(cond) + 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 index 09296f4e..c680f15b 100644 --- a/lib/ajax-datatables-rails/datatable/column/date_filter.rb +++ b/lib/ajax-datatables-rails/datatable/column/date_filter.rb @@ -5,6 +5,8 @@ module Datatable class Column module DateFilter + RANGE_DELIMITER = '-' + class DateRange attr_reader :begin, :end @@ -20,55 +22,44 @@ def exclude_end? # Add delimiter option to handle range search def delimiter - @view_column[:delimiter] || '-' - end - - def empty_range_search? - (formated_value == delimiter) || (range_start.blank? && range_end.blank?) + @delimiter ||= @view_column.fetch(:delimiter, RANGE_DELIMITER) end # A range value is in form '' # This returns def range_start - @range_start ||= formated_value.split(delimiter)[0] + @range_start ||= formatted_value.split(delimiter)[0] end # A range value is in form '' # This returns def range_end - @range_end ||= formated_value.split(delimiter)[1] + @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 non_regex_search - if cond == :date_range - date_range_search - else - super - end - end - def range_start_casted range_start.blank? ? parse_date('01/01/1970') : parse_date(range_start) end def range_end_casted - range_end.blank? ? Time.current : parse_date("#{range_end} 23:59:59") + range_end.blank? ? parse_date('9999-12-31 23:59:59') : parse_date("#{range_end} 23:59:59") end def parse_date(date) - if Time.zone - Time.zone.parse(date) - else - Time.parse(date) - end + Time.zone ? Time.zone.parse(date) : Time.parse(date) end end diff --git a/lib/ajax-datatables-rails/datatable/column/order.rb b/lib/ajax-datatables-rails/datatable/column/order.rb index 2c784781..3806e494 100644 --- a/lib/ajax-datatables-rails/datatable/column/order.rb +++ b/lib/ajax-datatables-rails/datatable/column/order.rb @@ -11,7 +11,7 @@ def orderable? # Add sort_field option to allow overriding of sort field def sort_field - @view_column[:sort_field] || field + @view_column.fetch(:sort_field, field) end def sort_query diff --git a/lib/ajax-datatables-rails/datatable/column/search.rb b/lib/ajax-datatables-rails/datatable/column/search.rb index e3371134..581165fb 100644 --- a/lib/ajax-datatables-rails/datatable/column/search.rb +++ b/lib/ajax-datatables-rails/datatable/column/search.rb @@ -5,16 +5,21 @@ 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[:cond] || :like + @view_column.fetch(:cond, :like) end def filter - @view_column[:cond].call(self, formated_value) + @view_column[:cond].call(self, formatted_value) end def search @@ -43,45 +48,77 @@ def use_regex? # 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)) + ::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 - numeric_search - when :null_value - null_value_search + searchable_integer? ? raw_search(cond) : empty_search when :start_with - casted_column.matches("#{formated_value}%") + text_search("#{formatted_value}%") when :end_with - casted_column.matches("%#{formated_value}") + text_search("%#{formatted_value}") when :like - casted_column.matches("%#{formated_value}%") + 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 formated_value == '!NULL' + if formatted_value == NOT_NULL_VALUE table[field].not_eq(nil) else table[field].eq(nil) end end - def numeric_search - if custom_field? - ::Arel::Nodes::SqlLiteral.new(field).eq(formated_value) + 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 - table[field].send(cond, formated_value) + 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 diff --git a/lib/ajax-datatables-rails/datatable/datatable.rb b/lib/ajax-datatables-rails/datatable/datatable.rb index c2db1d50..0d4e4d6f 100644 --- a/lib/ajax-datatables-rails/datatable/datatable.rb +++ b/lib/ajax-datatables-rails/datatable/datatable.rb @@ -2,15 +2,16 @@ module AjaxDatatablesRails module Datatable - - TRUE_VALUE = 'true' - 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 -------------------- @@ -43,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 @@ -70,13 +71,25 @@ def page 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 41e8b934..b817a586 100644 --- a/lib/ajax-datatables-rails/datatable/simple_order.rb +++ b/lib/ajax-datatables-rails/datatable/simple_order.rb @@ -4,19 +4,19 @@ 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) - if sort_nulls_last? - "CASE WHEN #{sort_column} IS NULL THEN 1 ELSE 0 END, #{sort_column} #{direction}" - else - "#{sort_column} #{direction}" - end + [sort_column, direction, nulls_last_sql].compact.join(' ') end def column @@ -24,7 +24,7 @@ def column end def direction - DIRECTIONS.find { |dir| dir == column_direction } || 'ASC' + DIRECTIONS.find { |dir| dir == column_direction } || DIRECTION_ASC end private @@ -38,7 +38,32 @@ def column_direction end def sort_nulls_last? - column.nulls_last? || AjaxDatatablesRails.config.nulls_last == true + 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 diff --git a/lib/ajax-datatables-rails/datatable/simple_search.rb b/lib/ajax-datatables-rails/datatable/simple_search.rb index a2ccd233..70bee016 100644 --- a/lib/ajax-datatables-rails/datatable/simple_search.rb +++ b/lib/ajax-datatables-rails/datatable/simple_search.rb @@ -4,6 +4,8 @@ module AjaxDatatablesRails module Datatable class SimpleSearch + TRUE_VALUE = 'true' + def initialize(options = {}) @options = options 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 9836a5b8..8da0895d 100644 --- a/lib/ajax-datatables-rails/orm/active_record.rb +++ b/lib/ajax-datatables-rails/orm/active_record.rb @@ -4,15 +4,11 @@ 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 + # rubocop:disable Style/EachWithObject, Style/SafeNavigation def sort_records(records) sort_by = datatable.orders.inject([]) do |queries, order| column = order.column @@ -21,7 +17,7 @@ def sort_records(records) end records.order(Arel.sql(sort_by.join(', '))) end - # rubocop:enable Style/EachWithObject + # rubocop:enable Style/EachWithObject, Style/SafeNavigation def paginate_records(records) records.offset(datatable.offset).limit(datatable.per_page) @@ -30,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 f918a55a..3e9a1320 100644 --- a/lib/ajax-datatables-rails/version.rb +++ b/lib/ajax-datatables-rails/version.rb @@ -1,5 +1,17 @@ # frozen_string_literal: true module AjaxDatatablesRails - VERSION = '0.4.1' + + 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/ajax_datatables_rails.rb b/lib/ajax_datatables_rails.rb deleted file mode 100644 index 49bc773d..00000000 --- a/lib/ajax_datatables_rails.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -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/search' - require 'ajax-datatables-rails/datatable/column/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/generators/datatable/config_generator.rb b/lib/generators/datatable/config_generator.rb deleted file mode 100644 index 2df3f2d8..00000000 --- a/lib/generators/datatable/config_generator.rb +++ /dev/null @@ -1,19 +0,0 @@ -# frozen_string_literal: true - -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 diff --git a/spec/ajax-datatables-rails/base_spec.rb b/spec/ajax-datatables-rails/base_spec.rb deleted file mode 100644 index 55020345..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_data' do - it 'should return a hash' do - create_list(:user, 5) - expect(datatable).to receive(:additional_data){ { 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]' do - paginated_view = double('view', params: { start: '11' }) - datatable = described_class.new(paginated_view) - expect(datatable.datatable.send(:offset)).to eq(11) - 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 7e540d2a..00000000 --- a/spec/ajax-datatables-rails/datatable/column_spec.rb +++ /dev/null @@ -1,154 +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 sort nulls last' do - expect(column.nulls_last?).to eq(false) - 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 '#data' do - it 'should return the data from params' do - expect(column.data).to eq 'username' - end - end - - describe '#source' do - it 'should return 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 'should return the corresponding AR table' do - expect(column.table).to eq User.arel_table - end - end - context 'with other ORM' do - it 'should return the corresponding model' do - expect(User).to receive(:respond_to?).with(:arel_table).and_return(false) - expect(column.table).to eq User - end - end - end - - describe '#model' do - it 'should return the corresponding AR model' do - expect(column.model).to eq User - end - end - - describe '#field' do - it 'should return the corresponding field in DB' do - expect(column.field).to eq :username - end - end - - describe '#custom_field?' do - it 'should return 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 '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 - - unless AjaxDatatablesRails.old_rails? - describe '#delimiter' do - it 'should be - by default' do - expect(column.delimiter).to eq('-') - end - 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 de530e5d..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(50) - 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 9c851eb0..00000000 --- a/spec/ajax-datatables-rails/datatable/simple_order_spec.rb +++ /dev/null @@ -1,40 +0,0 @@ -require 'spec_helper' - -describe AjaxDatatablesRails::Datatable::SimpleOrder do - - let(:view) { double('view', params: sample_params) } - let(:datatable) { ComplexDatatable.new(view).datatable } - let(:options) { ActiveSupport::HashWithIndifferentAccess.new({'column' => '1', 'dir' => 'desc'}) } - let(:simple_order) { AjaxDatatablesRails::Datatable::SimpleOrder.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 global option' do - before { AjaxDatatablesRails.config.nulls_last = true } - after { AjaxDatatablesRails.config.nulls_last = false } - - it 'sql query' do - expect(simple_order.query('email')).to eq( - 'CASE WHEN email IS NULL THEN 1 ELSE 0 END, email DESC' - ) - end - end - - describe 'using column option' do - let(:sorted_datatable) { DatatableOrderNullsLast.new(view).datatable } - let(:nulls_last_order) { AjaxDatatablesRails::Datatable::SimpleOrder.new(sorted_datatable, options) } - - it 'sql query' do - expect(nulls_last_order.query('email')).to eq( - 'CASE WHEN email IS NULL THEN 1 ELSE 0 END, email DESC' - ) - end - 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 32ac59b3..00000000 --- a/spec/ajax-datatables-rails/orm/active_record_filter_records_spec.rb +++ /dev/null @@ -1,498 +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 '#type_cast 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(:type_cast)).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(:type_cast)).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(:type_cast)).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(:type_cast)).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(:type_cast)).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(:type_cast)).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(:type_cast)).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(:type_cast)).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(:type_cast)).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 - - if AjaxDatatablesRails.config.db_adapter == :oracleenhanced - context 'when db_adapter is oracleenhanced' do - 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 - else - 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 - 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_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 784d85cf..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..2d6a4bb6 --- /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( + "CAST(\"USERS\".\"USERNAME\" AS VARCHAR2(4000)) LIKE '%doe%' AND CAST(\"USERS\".\"EMAIL\" AS VARCHAR2(4000)) LIKE '%example%'" + ) + 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 68% 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 d373d8fc..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.16 4.2.10] + 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,8 +36,8 @@ 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.16 4.2.10] + if RunningSpec.oracle? + if Rails.version.in? %w[4.2.11] expect(datatable.paginate_records(records).to_sql).to include( 'rownum <= 51' ) @@ -53,13 +54,15 @@ 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 similarity index 76% rename from spec/ajax-datatables-rails/orm/active_record_sort_records_spec.rb rename to spec/ajax_datatables_rails/orm/active_record_sort_records_spec.rb index 128fceb8..369d859c 100644 --- a/spec/ajax-datatables-rails/orm/active_record_sort_records_spec.rb +++ b/spec/ajax_datatables_rails/orm/active_record_sort_records_spec.rb @@ -1,13 +1,14 @@ +# 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(:nulls_last_datatable) { DatatableOrderNullsLast.new(view) } + let(:datatable) { ComplexDatatable.new(sample_params) } + let(:nulls_last_datatable) { DatatableOrderNullsLast.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 @@ -31,7 +32,7 @@ ) end - it 'should not sort a column which is not orderable' do + 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' } @@ -46,30 +47,32 @@ end describe '#sort_records with nulls last using global config' do - before { AjaxDatatablesRails.config.nulls_last = true } - after { AjaxDatatablesRails.config.nulls_last = false } - + 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 CASE WHEN users.username IS NULL THEN 1 ELSE 0 END, users.username ASC, ' + - 'CASE WHEN users.email IS NULL THEN 1 ELSE 0 END, users.email DESC' + "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, ' + - 'CASE WHEN users.email IS NULL THEN 1 ELSE 0 END, users.email DESC' + "ORDER BY users.username ASC, users.email DESC #{nulls_last_sql(datatable)}" ) 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..7f6a6759 --- /dev/null +++ b/spec/dummy/config/database.yml @@ -0,0 +1,25 @@ +<% adapter = ENV.fetch('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/xe' + username: <%= ENV.fetch('USER') %> + password: <%= ENV.fetch('USER') %> + database: 'xe' +<% 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 5cd5bf6a..bcecd98d 100644 --- a/spec/factories/user.rb +++ b/spec/factories/user.rb @@ -1,9 +1,11 @@ +# 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 index cec3ac49..07537f79 100755 --- a/spec/install_oracle.sh +++ b/spec/install_oracle.sh @@ -4,9 +4,15 @@ wget 'https://github.com/cbandy/travis-oracle/archive/v2.0.3.tar.gz' mkdir -p ~/.travis/oracle tar xz --strip-components 1 -C ~/.travis/oracle -f v2.0.3.tar.gz -~/.travis/oracle/download.sh +if [ -n ${CUSTOM_ORACLE_FILE} ]; then + wget -q ${CUSTOM_ORACLE_FILE} -O ~/.travis/oracle/oracle-xe-11.2.0-1.0.x86_64.rpm.zip +else + ~/.travis/oracle/download.sh +fi + ~/.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/datatable_order_nulls_last.rb b/spec/support/datatables/datatable_order_nulls_last.rb similarity index 80% rename from spec/support/datatable_order_nulls_last.rb rename to spec/support/datatables/datatable_order_nulls_last.rb index f4d7f6d3..e1b3acdc 100644 --- a/spec/support/datatable_order_nulls_last.rb +++ b/spec/support/datatables/datatable_order_nulls_last.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class DatatableOrderNullsLast < ComplexDatatable def view_columns super.deep_merge(email: { nulls_last: true }) 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 ffe760e8..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', orderable: false }, - 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