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