├── .github └── workflows │ └── test.yml ├── .gitignore ├── Appraisals ├── CHANGELOG.md ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── active_sort_order.gemspec ├── lib ├── active_sort_order.rb └── active_sort_order │ ├── concerns │ └── sort_order_concern.rb │ └── version.rb └── test ├── dummy_app ├── Rakefile ├── app │ ├── assets │ │ ├── config │ │ │ └── manifest.js │ │ ├── javascripts │ │ │ └── application.js │ │ └── stylesheets │ │ │ └── application.css │ ├── controllers │ │ └── application_controller.rb │ ├── mailers │ │ └── .gitkeep │ ├── models │ │ ├── application_record.rb │ │ ├── post.rb │ │ ├── post_with_base_order_a.rb │ │ ├── post_with_base_order_a_and_b.rb │ │ ├── post_with_base_order_b.rb │ │ ├── post_with_base_order_b_and_a.rb │ │ └── post_with_volatile_base_order.rb │ └── views │ │ └── layouts │ │ └── application.html.erb ├── config.ru ├── config │ ├── application.rb │ ├── boot.rb │ ├── database.yml │ ├── environment.rb │ ├── environments │ │ └── test.rb │ ├── initializers │ │ ├── backtrace_silencers.rb │ │ ├── inflections.rb │ │ ├── mime_types.rb │ │ ├── secret_token.rb │ │ ├── session_store.rb │ │ └── wrap_parameters.rb │ ├── locales │ │ └── en.yml │ ├── routes.rb │ └── secrets.yml ├── db │ ├── migrate │ │ └── 20210128155312_set_up_test_tables.rb │ └── schema.rb └── log │ └── .gitkeep ├── test_helper.rb └── unit ├── active_sort_order_test.rb └── errors_test.rb /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: 3 | push: 4 | branches: ['master'] 5 | pull_request: 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | 11 | env: 12 | RAILS_ENV: test 13 | 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | include: 18 | ### TEST RUBY VERSIONS 19 | - ruby: "2.6" 20 | - ruby: "2.7" 21 | - ruby: "3.0" 22 | db_gem_version: "~> 1.4" # fixes sqlite3 gem dependency issue 23 | - ruby: "3.1" 24 | - ruby: "3.2" 25 | - ruby: "3.3" 26 | - ruby: "3.4" 27 | ### TEST RAILS VERSIONS 28 | - ruby: "2.6" 29 | rails_version: "~> 5.2.0" 30 | - ruby: "2.6" 31 | rails_version: "~> 6.0.0" 32 | - ruby: "2.6" 33 | rails_version: "~> 6.1.0" 34 | - ruby: "3.3" 35 | rails_version: "~> 7.0.0" 36 | db_gem_version: "~> 1.4" # fixes sqlite3 gem dependency issue 37 | - ruby: "3.4" 38 | rails_version: "~> 7.1.0" 39 | - ruby: "3.4" 40 | rails_version: ~> "7.2.0" 41 | - ruby: "3.4" 42 | rails_version: ~> "8.0.0" 43 | ### TEST NON-DEFAULT DATABASES 44 | - ruby: "3.3" 45 | db_gem: "mysql2" 46 | - ruby: "3.3" 47 | db_gem: "pg" 48 | 49 | services: 50 | mysql: 51 | image: ${{ (matrix.db_gem == 'mysql2' && 'mysql') || '' }} # conditional service 52 | env: 53 | MYSQL_ROOT_PASSWORD: password 54 | MYSQL_DATABASE: test 55 | options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 56 | ports: ['3306:3306'] 57 | postgres: 58 | image: ${{ (matrix.db_gem == 'pg' && 'postgres') || '' }} # conditional service 59 | env: 60 | POSTGRES_USER: postgres 61 | POSTGRES_PASSWORD: password 62 | POSTGRES_DB: test 63 | ports: ['5432:5432'] 64 | 65 | steps: 66 | - uses: actions/checkout@v3 67 | 68 | - name: Set env DATABASE_URL 69 | run: | 70 | if [[ "${{ matrix.db_gem }}" == 'mysql2' ]]; then 71 | echo "DATABASE_URL=mysql2://root:password@127.0.0.1:3306/test" >> "$GITHUB_ENV" 72 | elif [[ "${{ matrix.db_gem }}" == 'pg' ]]; then 73 | echo "DATABASE_URL=postgres://postgres:password@localhost:5432/test" >> "$GITHUB_ENV" 74 | fi 75 | 76 | - name: Set env variables 77 | run: | 78 | echo "RAILS_VERSION=${{ matrix.rails_version }}" >> "$GITHUB_ENV" 79 | echo "DB_GEM=${{ matrix.db_gem }}" >> "$GITHUB_ENV" 80 | echo "DB_GEM_VERSION=${{ matrix.db_gem_version }}" >> "$GITHUB_ENV" 81 | 82 | - name: Install ruby 83 | uses: ruby/setup-ruby@v1 84 | with: 85 | ruby-version: "${{ matrix.ruby }}" 86 | bundler-cache: false ### not compatible with ENV-style Gemfile 87 | 88 | - name: Run test 89 | run: | 90 | bundle install 91 | RUBYOPT='--enable-frozen-string-literal' bundle exec rake test 92 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | gemfiles/*.lock 9 | .DS_Store 10 | ~.* 11 | ~* 12 | Gemfile.lock 13 | 14 | /tmp/ 15 | /test/**/tmp/ 16 | 17 | test/dummy_app/**/*.sqlite3* 18 | test/dummy_app/**/*.sqlite3 19 | test/dummy_app/**/*.log 20 | -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | ["sqlite3", "mysql2", "pg"].each do |db_gem| 2 | 3 | appraise "rails_7.0.#{db_gem}" do 4 | gem "rails", "~> 7.0.0" 5 | gem db_gem 6 | end 7 | 8 | appraise "rails_6.1.#{db_gem}" do 9 | gem "rails", "~> 6.1.1" 10 | gem db_gem 11 | end 12 | 13 | appraise "rails_6.0.#{db_gem}" do 14 | gem "rails", "~> 6.0.3" 15 | gem db_gem 16 | end 17 | 18 | appraise "rails_5.2.#{db_gem}" do 19 | gem "rails", "~> 5.2.4" 20 | gem db_gem 21 | end 22 | 23 | appraise "rails_5.1.#{db_gem}" do 24 | gem "rails", "~> 5.1.7" 25 | gem db_gem 26 | end 27 | 28 | appraise "rails_5.0.#{db_gem}" do 29 | gem "rails", "~> 5.0.7" 30 | 31 | if db_gem == 'sqlite3' 32 | gem "sqlite3", "~> 1.3.13" 33 | else 34 | gem db_gem 35 | end 36 | end 37 | 38 | end 39 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | --------- 3 | 4 | - **Unreleased - [View Diff](https://github.com/westonganger/active_sort_order/compare/v0.9.5...master)** 5 | * Nothing yet 6 | 7 | - **v0.9.5 - Feb 13, 2023 - [View Diff](https://github.com/westonganger/active_sort_order/compare/v0.9.4...v0.9.5)** 8 | * [#1](https://github.com/westonganger/active_sort_order/pull/1) - Utilize active_record lazy loading 9 | 10 | - **v0.9.4 - Dec 20, 2021 - [View Diff](https://github.com/westonganger/active_sort_order/compare/v0.9.3...v0.9.4)** 11 | * Improve error handling for sort_col_sql argument 12 | 13 | - **v0.9.3 - Oct 13, 2021 - [View Diff](https://github.com/westonganger/active_sort_order/compare/v0.9.2...v0.9.3)** 14 | * Allow ability to sort on multiple fields 15 | 16 | - **v0.9.2 - Apr 28, 2021 - [View Diff](https://github.com/westonganger/active_sort_order/compare/v0.9.1...v0.9.2)** 17 | * Fix deprecation warning in Rails 6.1 for `reorder(nil)` 18 | 19 | - **v0.9.1 - Jan 31, 2021 - [View Diff](https://github.com/westonganger/active_sort_order/compare/v0.9.0...v0.9.1)** 20 | * General Improvements 21 | * Add Github Actions CI supporting multiple version of Ruby, Rails and multiple databases types 22 | * Fix bugs with Rails 5.0 and 5.1 23 | * Limit supports to Rails 5.0+. Tests were showing some failures with Rails 4.2 so I dropped it. 24 | 25 | - **v0.9.0 - Jan 29, 2021 - [View Diff](https://github.com/westonganger/active_sort_order/compare/371fc82...v0.9.0)** 26 | * Initial Release 27 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | gem "rake" 6 | gem "minitest" 7 | gem 'minitest-reporters' 8 | 9 | if RUBY_VERSION.to_f >= 2.4 10 | gem 'warning' 11 | end 12 | 13 | def get_env(name) 14 | (ENV[name] && !ENV[name].empty?) ? ENV[name] : nil 15 | end 16 | 17 | gem 'rails', get_env("RAILS_VERSION") 18 | 19 | db_gem = get_env("DB_GEM") || "sqlite3" 20 | gem db_gem, get_env("DB_GEM_VERSION") 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Weston Ganger 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Active Sort Order 2 | 3 | Gem Version 4 | CI Status 5 | RubyGems Downloads 6 | 7 | The "easy-peasy" dynamic sorting pattern for ActiveRecord that your Rails apps deserve. Useful for Rails controllers with large data, pagination, etc. 8 | 9 | Features: 10 | 11 | - Full SQL compatibility 12 | - Dead Simple. Just [one concern with one scope](#additional-customizations). 13 | 14 | ## Installation 15 | 16 | ```ruby 17 | gem 'active_sort_order' 18 | ``` 19 | 20 | Then add `include ActiveSortOrder` to your ApplicationRecord or individual models. 21 | 22 | ```ruby 23 | class ApplicationRecord < ActiveRecord::Base 24 | include ActiveSortOrder 25 | end 26 | ``` 27 | 28 | ## Dynamic Sorting 29 | 30 | This gem defines one scope on your models: `sort_order` 31 | 32 | This method uses ActiveRecord's `reorder` under the hood, so any previously defined `order` will be removed upon calling `sort_order` 33 | 34 | In the below examples we are within a controller and are using the params as our variables: 35 | 36 | ```ruby 37 | # app/controllers/posts_controller.rb 38 | 39 | case params[:sort] 40 | when "number_str" 41 | sort_col_sql = "CAST(posts.number_str AS int)" 42 | when "user" 43 | ### To sort on multiple fields pass in an Array 44 | sort_col_sql = ["users.first_name", "users.last_name"] 45 | else 46 | sort_col_sql = params[:sort] 47 | end 48 | 49 | ### Output combined sort order (if present) and secondary / base sort order 50 | Post.all.sort_order(sort_col_sql, params[:direction], base_sort_order: "lower(number) ASC, lower(code) ASC") 51 | 52 | ### Output combined sort order (if present) AND applies the classes base_sort_order (if defined) 53 | Post.all.sort_order(sort_col_sql, params[:direction]) 54 | ``` 55 | 56 | ## Sorting on multiple columns 57 | 58 | ##### Method Definition: 59 | 60 | `sort_order(sort_col_sql = nil, sort_direction_sql = nil, base_sort_order: true)` 61 | 62 | Options: 63 | 64 | - `sort_col_sql` is a SQL String of the column name 65 | * Feel free to use any SQL manipulation on the column name 66 | * There is no built-in SQL string validation so be sure to handle your sanitization in your project before passing to this method. See [Safely Handling Input](#safely-handling-input) 67 | * If blank value provided it will skip the dynamic sort and just apply the `base_sort_order` 68 | - `sort_direction_sql` is a String of the SQL ORDER BY direction 69 | * The SQL String is automatically validated within the few allowable SQL ORDER BY directions. 70 | * If nil or "blank string" provided it will fallback to "ASC" 71 | - `base_sort_order` is a String of the SQL base ordering 72 | * If not provided or true it will use the classes `base_sort_order` method (if defined) 73 | * If nil or false is provided it will skip the classes `base_sort_order` 74 | 75 | ## Base Sort Order 76 | 77 | To maintain consistency when sorting its always a good idea to have a secondary or base sort order for when duplicates of the main sort column are found or no sort is provided. 78 | 79 | For this you can define a `base_sort_order` class method to your models. 80 | 81 | This will be utilized on the `sort_order` method when not providing a direct `:base_sort_order` argument. 82 | 83 | ```ruby 84 | class Post < ActiveRecord::Base 85 | include ActiveSortOrder 86 | 87 | def self.base_sort_order 88 | "lower(#{table_name}.name) ASC, lower(#{table_name}.code) ASC" # for example 89 | end 90 | 91 | end 92 | ``` 93 | 94 | The default behaviours of this are shown below. 95 | 96 | ```ruby 97 | ### Applies the classes base_sort_order (if defined) 98 | Post.all.sort_order 99 | 100 | ### Override the classes base_sort_order 101 | Post.all.sort_order(base_sort_order: "lower(number) DESC") 102 | # OR 103 | Post.all.sort_order(sort_col_sql, params[:direction], base_sort_order: "lower(number) DESC") 104 | 105 | ### Skip the classes base_sort_order by providing false, nil will still use classes base_sort_order 106 | Post.all.sort_order(sort_col_sql, params[:direction], base_sort_order: false) 107 | ``` 108 | 109 | ## Safely Handling Input 110 | 111 | When accepting params or any custom input for column names it is wise to safely map the field name/alias to the correct SQL string rather than directly sending in the params. 112 | 113 | Here is an example on how to handle this within your controller: 114 | 115 | ```ruby 116 | if params[:sort].present? 117 | case params[:sort] 118 | when "author_name" 119 | sort_col_sql = "authors.name" 120 | when "a_or_b" 121 | sort_col_sql = "COALESCE(posts.field_a, posts.field_b)" 122 | when "price" 123 | sort_col_sql = "CAST(REPLACE(posts.price, '$', ',', '') AS int)" 124 | else 125 | raise "Invalid Sort Column Given: #{params[:sort]}" 126 | end 127 | end 128 | 129 | Post.all.sort_order(sort_col_sql, params[:direction]) 130 | ``` 131 | 132 | ## Additional Customizations 133 | 134 | This gem is just one concern with one scope. I encourage you to read the code for this library to understand how it works within your project so that you are capable of customizing the functionality later. You can always copy the code directly into your project for deeper project-specific customizations. 135 | 136 | - [lib/active_sort_order/concerns/sort_order_concern.rb](./lib/active_sort_order/concerns/sort_order_concern.rb) 137 | 138 | ## Helper / View Examples 139 | 140 | We do not provide built in helpers or view templates because this is a major restriction to applications. Instead we provide a simple copy-and-pasteable starter template for the sort link: 141 | 142 | ```ruby 143 | ### app/helpers/application_helper.rb 144 | 145 | module ApplicationHelper 146 | 147 | def sort_link(column, title = nil, opts = {}) 148 | column = column.to_s 149 | 150 | if title && title.is_a?(Hash) 151 | opts = title 152 | title = opts[:title] 153 | end 154 | 155 | title ||= column.titleize 156 | 157 | if opts[:disabled] 158 | return title 159 | else 160 | if params[:direction].present? && params[:sort].present? 161 | direction = (column == params[:sort] && params[:direction] == "asc") ? "desc" : "asc" 162 | else 163 | direction = "asc" 164 | end 165 | 166 | return link_to(title, params.to_unsafe_h.merge(sort: column, direction: direction)) 167 | end 168 | end 169 | 170 | end 171 | ``` 172 | 173 | Then use the link helper within your views like: 174 | 175 | ```erb 176 | 177 | <%= sort_link :name %> 178 | 179 | 180 | 181 | <%= sort_link "companies.name", "Company Name" %> 182 | 183 | 184 | 185 | <%= sort_link "companies.name", "Company Name", disabled: !@sort_enabled %> 186 | 187 | ``` 188 | 189 | ## Credits 190 | 191 | Created & Maintained by [Weston Ganger](https://westonganger.com) - [@westonganger](https://github.com/westonganger) 192 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/lib/active_sort_order/version.rb') 2 | 3 | require "bundler/gem_tasks" 4 | require "rake/testtask" 5 | 6 | Rake::TestTask.new(:test) do |t| 7 | t.libs << "test" 8 | t.libs << "lib" 9 | t.test_files = FileList["test/**/*_test.rb"] 10 | end 11 | 12 | task default: [:test] 13 | 14 | task :console do 15 | require 'active_sort_order' 16 | 17 | require_relative 'test/dummy_app/app/models/application_record.rb' 18 | require_relative 'test/dummy_app/app/models/post.rb' 19 | Dir.glob("test/dummy_app/app/models/*.rb").each do |f| 20 | require_relative(f) 21 | end 22 | 23 | require 'irb' 24 | binding.irb 25 | end 26 | -------------------------------------------------------------------------------- /active_sort_order.gemspec: -------------------------------------------------------------------------------- 1 | require_relative 'lib/active_sort_order/version' 2 | 3 | Gem::Specification.new do |s| 4 | s.name = "active_sort_order" 5 | s.version = ActiveSortOrder::VERSION 6 | s.authors = ["Weston Ganger"] 7 | s.email = ["weston@westonganger.com"] 8 | 9 | s.summary = "The \"easy-peasy\" dynamic sorting pattern for ActiveRecord that your Rails apps deserve." 10 | s.description = s.summary 11 | s.homepage = "https://github.com/westonganger/active_sort_order" 12 | s.license = "MIT" 13 | 14 | s.metadata["source_code_uri"] = s.homepage 15 | s.metadata["changelog_uri"] = File.join(s.homepage, "blob/master/CHANGELOG.md") 16 | 17 | s.files = Dir.glob("{lib/**/*}") + %w{ LICENSE README.md Rakefile CHANGELOG.md } 18 | s.require_path = 'lib' 19 | 20 | s.add_runtime_dependency "activerecord", '>= 5' 21 | end 22 | -------------------------------------------------------------------------------- /lib/active_sort_order.rb: -------------------------------------------------------------------------------- 1 | require "active_sort_order/version" 2 | 3 | require "active_support/lazy_load_hooks" 4 | 5 | ActiveSupport.on_load(:active_record) do 6 | require "active_sort_order/concerns/sort_order_concern" 7 | 8 | module ActiveSortOrder 9 | extend ActiveSupport::Concern 10 | 11 | included do 12 | include ActiveSortOrder::SortOrderConcern 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/active_sort_order/concerns/sort_order_concern.rb: -------------------------------------------------------------------------------- 1 | module ActiveSortOrder 2 | module SortOrderConcern 3 | extend ActiveSupport::Concern 4 | 5 | included do 6 | 7 | scope :sort_order, ->(sort_col_sql = nil, sort_direction_sql = nil, base_sort_order: true){ 8 | if !sort_col_sql.is_a?(Array) 9 | sort_col_sql = [sort_col_sql].compact 10 | end 11 | 12 | sort_col_sql.each_with_index do |x, i| 13 | if [String, Symbol].exclude?(x.class) 14 | raise ArgumentError.new("Invalid first argument `sort_col_sql`, expecting a String or Symbol or Array") 15 | else 16 | sort_col_sql[i] = x.to_s 17 | end 18 | end 19 | 20 | if sort_col_sql.present? 21 | ### SORT DIRECTION HANDLING 22 | if sort_direction_sql.is_a?(Symbol) 23 | sort_direction_sql = sort_direction_sql.to_s.gsub('_', ' ') 24 | end 25 | 26 | if sort_direction_sql.nil? || (sort_direction_sql.is_a?(String) && sort_direction_sql == "") 27 | sort_direction_sql = "ASC" 28 | elsif !sort_direction_sql.is_a?(String) 29 | raise ArgumentError.new("Invalid second argument `sort_direction_sql`, expecting a String or Symbol") 30 | else 31 | valid_directions = [ 32 | "ASC", 33 | "DESC", 34 | "ASC NULLS FIRST", 35 | "ASC NULLS LAST", 36 | "DESC NULLS FIRST", 37 | "DESC NULLS LAST", 38 | ].freeze 39 | 40 | orig_direction_sql = sort_direction_sql 41 | 42 | ### REMOVE DUPLICATE BLANKS - Apparently this also removes "\n" and "\t" 43 | sort_direction_sql = orig_direction_sql.split(' ').join(' ') 44 | 45 | if !valid_directions.include?(sort_direction_sql.upcase) 46 | raise ArgumentError.new("Invalid second argument `sort_direction_sql`: #{orig_direction_sql}") 47 | end 48 | end 49 | 50 | sql_str = sort_col_sql.map{|x| "#{x} #{sort_direction_sql}" }.join(", ") 51 | end 52 | 53 | ### BASE SORT ORDER HANDLING 54 | if base_sort_order == true 55 | if self.respond_to?(:base_sort_order) 56 | base_sort_order = self.base_sort_order 57 | 58 | if [String, NilClass, FalseClass].exclude?(base_sort_order.class) 59 | raise ArgumentError.new("Invalid value returned from class method `base_sort_order`") 60 | end 61 | else 62 | base_sort_order = nil 63 | end 64 | elsif base_sort_order && !base_sort_order.is_a?(String) 65 | raise ArgumentError.new("Invalid argument provided for :base_sort_order") 66 | end 67 | 68 | if base_sort_order.present? 69 | if sql_str.present? 70 | sql_str << ", #{base_sort_order}" 71 | else 72 | sql_str = base_sort_order 73 | end 74 | end 75 | 76 | if sql_str.blank? 77 | next self.where(nil) 78 | else 79 | sanitized_str = Arel.sql(sanitize_sql_for_order(sql_str)) 80 | 81 | next self.reorder(sanitized_str) 82 | end 83 | } 84 | 85 | end 86 | 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /lib/active_sort_order/version.rb: -------------------------------------------------------------------------------- 1 | module ActiveSortOrder 2 | VERSION = "0.9.5".freeze 3 | end 4 | -------------------------------------------------------------------------------- /test/dummy_app/Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | # Add your own tasks in files placed in lib/tasks ending in .rake, 3 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 4 | 5 | require File.expand_path('../config/application', __FILE__) 6 | 7 | Dummy::Application.load_tasks 8 | -------------------------------------------------------------------------------- /test/dummy_app/app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link_tree ../images 2 | //= link_directory ../javascripts .js 3 | //= link_directory ../stylesheets .css .scss 4 | -------------------------------------------------------------------------------- /test/dummy_app/app/assets/javascripts/application.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/westonganger/active_sort_order/6e1bbb5320d71c9e47eec512f697e95aff0c1f11/test/dummy_app/app/assets/javascripts/application.js -------------------------------------------------------------------------------- /test/dummy_app/app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | *= require_self 3 | */ 4 | -------------------------------------------------------------------------------- /test/dummy_app/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | protect_from_forgery 3 | end 4 | -------------------------------------------------------------------------------- /test/dummy_app/app/mailers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/westonganger/active_sort_order/6e1bbb5320d71c9e47eec512f697e95aff0c1f11/test/dummy_app/app/mailers/.gitkeep -------------------------------------------------------------------------------- /test/dummy_app/app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | self.abstract_class = true 3 | end 4 | -------------------------------------------------------------------------------- /test/dummy_app/app/models/post.rb: -------------------------------------------------------------------------------- 1 | class Post < ApplicationRecord 2 | include ActiveSortOrder 3 | end 4 | -------------------------------------------------------------------------------- /test/dummy_app/app/models/post_with_base_order_a.rb: -------------------------------------------------------------------------------- 1 | class PostWithBaseOrderA < Post 2 | 3 | def self.base_sort_order 4 | "posts.a ASC" 5 | end 6 | 7 | end 8 | -------------------------------------------------------------------------------- /test/dummy_app/app/models/post_with_base_order_a_and_b.rb: -------------------------------------------------------------------------------- 1 | class PostWithBaseOrderAAndB < Post 2 | 3 | def self.base_sort_order 4 | "posts.a ASC, posts.b ASC" 5 | end 6 | 7 | end 8 | -------------------------------------------------------------------------------- /test/dummy_app/app/models/post_with_base_order_b.rb: -------------------------------------------------------------------------------- 1 | class PostWithBaseOrderB < Post 2 | 3 | def self.base_sort_order 4 | "posts.b ASC" 5 | end 6 | 7 | end 8 | -------------------------------------------------------------------------------- /test/dummy_app/app/models/post_with_base_order_b_and_a.rb: -------------------------------------------------------------------------------- 1 | class PostWithBaseOrderBAndA < Post 2 | 3 | def self.base_sort_order 4 | "posts.b ASC, posts.a ASC" 5 | end 6 | 7 | end 8 | -------------------------------------------------------------------------------- /test/dummy_app/app/models/post_with_volatile_base_order.rb: -------------------------------------------------------------------------------- 1 | class PostWithVolatileBaseOrder < Post 2 | 3 | def self.base_sort_order 4 | # Will be overwritten in tests 5 | end 6 | 7 | end 8 | -------------------------------------------------------------------------------- /test/dummy_app/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Dummy App 5 | <%= stylesheet_link_tag "application" %> 6 | <%= javascript_include_tag "application" %> 7 | <%= csrf_meta_tags %> 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /test/dummy_app/config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require ::File.expand_path('../config/environment', __FILE__) 4 | run Dummy::Application 5 | -------------------------------------------------------------------------------- /test/dummy_app/config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../boot', __FILE__) 2 | 3 | require "logger" # Fix for Rails 7.0 and below, https://github.com/rails/rails/pull/54264 4 | 5 | require 'rails/all' 6 | 7 | Bundler.require 8 | 9 | module Dummy 10 | class Application < Rails::Application 11 | # Settings in config/environments/* take precedence over those specified here. 12 | # Application configuration should go into files in config/initializers 13 | # -- all .rb files in that directory are automatically loaded. 14 | 15 | # Custom directories with classes and modules you want to be autoloadable. 16 | # config.autoload_paths += %W(#{config.root}/extras) 17 | 18 | # Only load the plugins named here, in the order given (default is alphabetical). 19 | # :all can be used as a placeholder for all plugins not explicitly named. 20 | # config.plugins = [ :exception_notification, :ssl_requirement, :all ] 21 | 22 | # Activate observers that should always be running. 23 | # config.active_record.observers = :cacher, :garbage_collector, :forum_observer 24 | 25 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 26 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 27 | # config.time_zone = 'Central Time (US & Canada)' 28 | 29 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 30 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 31 | # config.i18n.default_locale = :de 32 | 33 | # Configure the default encoding used in templates for Ruby 1.9. 34 | config.encoding = "utf-8" 35 | 36 | # Configure sensitive parameters which will be filtered from the log file. 37 | config.filter_parameters += [:password] 38 | 39 | config.generators.test_framework = false 40 | config.generators.helper = false 41 | config.generators.stylesheets = false 42 | config.generators.javascripts = false 43 | 44 | config.after_initialize do 45 | ActiveRecord::Migration.migrate(Rails.root.join("db/migrate/*").to_s) 46 | end 47 | 48 | if ActiveRecord.respond_to?(:gem_version) 49 | gem_version = ActiveRecord.gem_version 50 | if gem_version.to_s.start_with?("5.2.") 51 | config.active_record.sqlite3.represent_boolean_as_integer = true 52 | end 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /test/dummy_app/config/boot.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | gemfile = File.expand_path('../../../../Gemfile', __FILE__) 3 | 4 | if File.exist?(gemfile) 5 | ENV['BUNDLE_GEMFILE'] = gemfile 6 | require 'bundler' 7 | Bundler.setup 8 | end 9 | 10 | $:.unshift File.expand_path('../../../../lib', __FILE__) -------------------------------------------------------------------------------- /test/dummy_app/config/database.yml: -------------------------------------------------------------------------------- 1 | default: &default 2 | <% if defined?(SQLite3) %> 3 | adapter: sqlite3 4 | database: db/test.sqlite3 5 | 6 | <% elsif defined?(Mysql2) %> 7 | adapter: mysql2 8 | database: active_sort_order_test 9 | 10 | <% elsif defined?(PG) %> 11 | adapter: postgresql 12 | database: active_sort_order_test 13 | 14 | <% end %> 15 | 16 | development: 17 | <<: *default 18 | 19 | test: 20 | <<: *default 21 | -------------------------------------------------------------------------------- /test/dummy_app/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the rails application 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the rails application 5 | Dummy::Application.initialize! 6 | -------------------------------------------------------------------------------- /test/dummy_app/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Dummy::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Configure static asset server for tests with Cache-Control for performance 11 | config.serve_static_files = true 12 | config.public_file_server.enabled = true 13 | 14 | # Log error messages when you accidentally call methods on nil 15 | config.whiny_nils = true 16 | 17 | # Show full error reports and disable caching 18 | config.consider_all_requests_local = true 19 | config.action_controller.perform_caching = false 20 | 21 | # Raise exceptions instead of rendering exception templates 22 | config.action_dispatch.show_exceptions = false 23 | 24 | # Disable request forgery protection in test environment 25 | config.action_controller.allow_forgery_protection = false 26 | 27 | # Tell Action Mailer not to deliver emails to the real world. 28 | # The :test delivery method accumulates sent emails in the 29 | # ActionMailer::Base.deliveries array. 30 | config.action_mailer.delivery_method = :test 31 | 32 | # Use SQL instead of Active Record's schema dumper when creating the test database. 33 | # This is necessary if your schema can't be completely dumped by the schema dumper, 34 | # like if you have constraints or database-specific column types 35 | # config.active_record.schema_format = :sql 36 | 37 | # Print deprecation notices to the stderr 38 | config.active_support.deprecation = :stderr 39 | 40 | config.eager_load = false 41 | end 42 | -------------------------------------------------------------------------------- /test/dummy_app/config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /test/dummy_app/config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format 4 | # (all these examples are active by default): 5 | # ActiveSupport::Inflector.inflections do |inflect| 6 | # inflect.plural /^(ox)$/i, '\1en' 7 | # inflect.singular /^(ox)en/i, '\1' 8 | # inflect.irregular 'person', 'people' 9 | # inflect.uncountable %w( fish sheep ) 10 | # end 11 | -------------------------------------------------------------------------------- /test/dummy_app/config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | # Mime::Type.register_alias "text/html", :iphone 6 | -------------------------------------------------------------------------------- /test/dummy_app/config/initializers/secret_token.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | # Make sure the secret is at least 30 characters and all random, 6 | # no regular words or you'll be exposed to dictionary attacks. 7 | 8 | gem_version = ActiveRecord.gem_version 9 | if gem_version <= Gem::Version.new("5.1") 10 | Dummy::Application.config.secret_token = '4f337f0063fbb4a724dd8da15419679300da990ae4f6c94d36c714a3cd07e9653fc42d902cf33a9b9449a28e7eb2673f928172d65a090fa3c9156d6beea8d16c' 11 | end 12 | -------------------------------------------------------------------------------- /test/dummy_app/config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Dummy::Application.config.session_store :cookie_store, key: '_dummy_session' 4 | 5 | # Use the database for sessions instead of the cookie-based default, 6 | # which shouldn't be used to store highly confidential information 7 | # (create the session table with "rails generate session_migration") 8 | # Dummy::Application.config.session_store :active_record_store 9 | -------------------------------------------------------------------------------- /test/dummy_app/config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | # 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] 9 | end 10 | 11 | # Disable root element in JSON by default. 12 | ActiveSupport.on_load(:active_record) do 13 | self.include_root_in_json = false 14 | end 15 | -------------------------------------------------------------------------------- /test/dummy_app/config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Sample localization file for English. Add more files in this directory for other locales. 2 | # See https://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points. 3 | 4 | en: 5 | hello: "Hello world" 6 | -------------------------------------------------------------------------------- /test/dummy_app/config/routes.rb: -------------------------------------------------------------------------------- 1 | Dummy::Application.routes.draw do 2 | get 'spreadsheets/csv', to: 'spreadsheets#csv' 3 | get 'spreadsheets/ods', to: 'spreadsheets#ods' 4 | get 'spreadsheets/xlsx', to: 'spreadsheets#xlsx' 5 | get 'spreadsheets/alt_xlsx', to: 'spreadsheets#alt_xlsx' 6 | end 7 | -------------------------------------------------------------------------------- /test/dummy_app/config/secrets.yml: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key is used for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | 6 | # Make sure the secret is at least 30 characters and all random, 7 | # no regular words or you'll be exposed to dictionary attacks. 8 | # You can use `rake secret` to generate a secure secret key. 9 | 10 | # Make sure the secrets in this file are kept private 11 | # if you're sharing your code publicly. 12 | 13 | development: 14 | secret_key_base: d28054e102cd55dcd684cee239d31ddf1e1acd83bd879dd5f671e989f5c9d94ec1ede00e7fcf9b6bde4cd115f93c54e3ba6c5dc05d233292542f27a79706fcb4 15 | 16 | test: 17 | secret_key_base: 378b4f2309d4898f5170b41624e19bf60ce8a154ad87c100e8846bddcf4c28b72b533f2e73738ef8f6eabb7a773a0a0e7c32c0649916c5f280eb7ac621fc318c 18 | 19 | # Do not keep production secrets in the repository, 20 | # instead read values from the environment. 21 | production: 22 | secret_key_base: 5e73c057b92f67f980fbea4c1c2c495b25def0048f8c1c040fed9c08f49cd50a2ebf872dd87857afc0861479e9382fceb7d9837a0bce546c2f7594e2f4da45e3 23 | -------------------------------------------------------------------------------- /test/dummy_app/db/migrate/20210128155312_set_up_test_tables.rb: -------------------------------------------------------------------------------- 1 | if defined?(ActiveRecord::Migration::Current) 2 | migration_klass = ActiveRecord::Migration::Current 3 | else 4 | migration_klass = ActiveRecord::Migration 5 | end 6 | 7 | class SetUpTestTables < migration_klass 8 | 9 | def change 10 | create_table :posts do |t| 11 | t.integer :a, :b 12 | end 13 | end 14 | 15 | end 16 | -------------------------------------------------------------------------------- /test/dummy_app/db/schema.rb: -------------------------------------------------------------------------------- 1 | # This file is auto-generated from the current state of the database. Instead 2 | # of editing this file, please use the migrations feature of Active Record to 3 | # incrementally modify your database, and then regenerate this schema definition. 4 | # 5 | # This file is the source Rails uses to define your schema when running `rails 6 | # db:schema:load`. When creating a new database, `rails db:schema:load` tends to 7 | # be faster and is potentially less error prone than running all of your 8 | # migrations from scratch. Old migrations may fail to apply correctly if those 9 | # migrations use external dependencies or application code. 10 | # 11 | # It's strongly recommended that you check this file into your version control system. 12 | 13 | ActiveRecord::Schema.define(version: 2020_10_01_061824) do 14 | 15 | create_table "posts", force: :cascade do |t| 16 | t.integer :a, :b 17 | end 18 | 19 | end 20 | -------------------------------------------------------------------------------- /test/dummy_app/log/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/westonganger/active_sort_order/6e1bbb5320d71c9e47eec512f697e95aff0c1f11/test/dummy_app/log/.gitkeep -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | #$LOAD_PATH.unshift File.expand_path("../lib", __dir__) 2 | ENV["RAILS_ENV"] = "test" 3 | 4 | require "active_sort_order" 5 | 6 | begin 7 | require 'warning' 8 | 9 | Warning.ignore( 10 | %r{mail/parsers/address_lists_parser}, ### Hide mail gem warnings 11 | ) 12 | rescue LoadError 13 | # Do nothing 14 | end 15 | 16 | ### Instantiates Rails 17 | require File.expand_path("../dummy_app/config/environment.rb", __FILE__) 18 | 19 | require "rails/test_help" 20 | 21 | class ActiveSupport::TestCase 22 | # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. 23 | fixtures :all 24 | end 25 | 26 | Rails.backtrace_cleaner.remove_silencers! 27 | 28 | require 'minitest/reporters' 29 | Minitest::Reporters.use!( 30 | Minitest::Reporters::DefaultReporter.new, 31 | ENV, 32 | Minitest.backtrace_filter 33 | ) 34 | 35 | require "minitest/autorun" 36 | 37 | # Run any available migration 38 | if ActiveRecord::VERSION::MAJOR == 6 39 | ActiveRecord::MigrationContext.new(File.expand_path("dummy_app/db/migrate/", __dir__), ActiveRecord::SchemaMigration).migrate 40 | else 41 | ActiveRecord::MigrationContext.new(File.expand_path("dummy_app/db/migrate/", __dir__)).migrate 42 | end 43 | 44 | [Post].each do |klass| 45 | if klass.connection.adapter_name.downcase.include?("sqlite") 46 | ActiveRecord::Base.connection.execute("DELETE FROM #{klass.table_name};") 47 | ActiveRecord::Base.connection.execute("UPDATE `sqlite_sequence` SET `seq` = 0 WHERE `name` = '#{klass.table_name}';") 48 | else 49 | ActiveRecord::Base.connection.execute("TRUNCATE TABLE #{klass.table_name}") 50 | end 51 | end 52 | 53 | DATA = {}.with_indifferent_access 54 | 55 | DATA[:posts] = [ 56 | Post.find_or_create_by!(a: 1, b: 3), 57 | Post.find_or_create_by!(a: 2, b: 2), 58 | Post.find_or_create_by!(a: 3, b: 2), 59 | Post.find_or_create_by!(a: 4, b: 1), 60 | Post.find_or_create_by!(a: 5, b: 1), 61 | ].shuffle 62 | -------------------------------------------------------------------------------- /test/unit/active_sort_order_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class ActiveSortOrderTest < ActiveSupport::TestCase 4 | 5 | setup do 6 | end 7 | 8 | teardown do 9 | end 10 | 11 | def test_exposes_main_module 12 | assert ActiveSortOrder.is_a?(Module) 13 | end 14 | 15 | def test_exposes_version 16 | assert ActiveSortOrder::VERSION 17 | end 18 | 19 | def test_base_sort_order_default_value 20 | klass = PostWithBaseOrderA 21 | 22 | assert PostWithBaseOrderA.unscoped.sort_order.to_sql.include?("ORDER BY #{klass.base_sort_order}") 23 | 24 | assert PostWithBaseOrderA.unscoped.sort_order(base_sort_order: true).to_sql.include?("ORDER BY #{klass.base_sort_order}") 25 | end 26 | 27 | def test_class_base_sort_order_only 28 | assert_equal Post.all.count, DATA[:posts].count 29 | 30 | sorted = PostWithBaseOrderA.all.sort_order 31 | 32 | expected = DATA[:posts].sort_by{|item| item.a } 33 | 34 | sorted.each_with_index do |item, i| 35 | assert_equal expected[i].id, item.id 36 | end 37 | 38 | sorted = PostWithBaseOrderB.all.sort_order 39 | 40 | expected = DATA[:posts].sort_by{|item| item.b } 41 | 42 | sorted.each_with_index do |item, i| 43 | assert_equal expected[i].b, item.b ### use b instead of id as its not unique 44 | end 45 | 46 | sorted = PostWithBaseOrderAAndB.all.sort_order 47 | 48 | expected = DATA[:posts].sort_by{|item| [item.a, item.b] } 49 | 50 | sorted.each_with_index do |item, i| 51 | assert_equal expected[i].id, item.id 52 | end 53 | 54 | sorted = PostWithBaseOrderBAndA.all.sort_order 55 | 56 | expected = DATA[:posts].sort_by{|item| [item.b, item.a] } 57 | 58 | sorted.each_with_index do |item, i| 59 | assert_equal expected[i].id, item.id 60 | end 61 | end 62 | 63 | def test_override_base_sort_order_only 64 | assert_equal Post.all.count, DATA[:posts].count 65 | 66 | sorted = PostWithBaseOrderA.order(b: :desc).sort_order(base_sort_order: "posts.b ASC") 67 | 68 | expected = DATA[:posts].sort_by{|item| item.b } 69 | 70 | sorted.each_with_index do |item, i| 71 | assert_equal expected[i].b, item.b ### use b instead of id as its not unique 72 | end 73 | 74 | expected = DATA[:posts].sort_by{|item| item.id } 75 | 76 | ### NIL & FALSE 77 | [nil, false].each do |v| 78 | sorted = PostWithBaseOrderA.order(id: :asc).sort_order(base_sort_order: v) 79 | 80 | sorted.each_with_index do |item, i| 81 | assert_equal expected[i].id, item.id 82 | end 83 | end 84 | end 85 | 86 | def test_sort_only 87 | assert_equal Post.all.count, DATA[:posts].count 88 | 89 | expected = DATA[:posts].sort_by{|item| item.a }.reverse 90 | 91 | sorted = PostWithBaseOrderA.all.sort_order(:a, :desc) 92 | 93 | sorted.each_with_index do |item, i| 94 | assert_equal expected[i].id, item.id 95 | end 96 | 97 | sorted = PostWithBaseOrderA.all.sort_order("posts.a", "DESC") 98 | 99 | sorted.each_with_index do |item, i| 100 | assert_equal expected[i].id, item.id 101 | end 102 | end 103 | 104 | def test_base_sort_order_and_sort 105 | assert_equal Post.all.count, DATA[:posts].count 106 | 107 | sorted = PostWithBaseOrderA.all.sort_order("posts.a", "DESC") 108 | 109 | expected = DATA[:posts].sort_by{|item| item.a }.reverse 110 | 111 | sorted.each_with_index do |item, i| 112 | assert_equal expected[i].id, item.id 113 | end 114 | 115 | sorted = PostWithBaseOrderB.all.sort_order("posts.b", "DESC") 116 | 117 | expected = DATA[:posts].sort_by{|item| item.b }.reverse 118 | 119 | sorted.each_with_index do |item, i| 120 | assert_equal expected[i].b, item.b ### use b instead of id as its not unique 121 | end 122 | end 123 | 124 | def test_sort_on_multiple_fields 125 | assert_equal Post.all.count, DATA[:posts].count 126 | 127 | expected = DATA[:posts].sort_by{|item| [item.b, item.a] } 128 | 129 | sorted = PostWithBaseOrderA.all.sort_order([:b, :a], :asc) 130 | 131 | sorted.each_with_index do |item, i| 132 | assert_equal expected[i].id, item.id 133 | end 134 | 135 | expected = DATA[:posts].sort_by{|item| [item.b, item.a] }.reverse 136 | 137 | sorted = PostWithBaseOrderA.all.sort_order([:b, :a], :desc) 138 | 139 | sorted.each_with_index do |item, i| 140 | assert_equal expected[i].id, item.id 141 | end 142 | end 143 | 144 | end 145 | -------------------------------------------------------------------------------- /test/unit/errors_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class ActiveSortOrderTest < ActiveSupport::TestCase 4 | 5 | setup do 6 | end 7 | 8 | teardown do 9 | end 10 | 11 | def test_sort_str_errors 12 | ### TEST VALID 13 | valid = [ 14 | "foo", 15 | :foo, 16 | nil, 17 | "", 18 | [], 19 | [:foo], 20 | ] 21 | 22 | valid.each do |v| 23 | Post.sort_order(v, :asc).limit(1) 24 | end 25 | 26 | ### TEST INVALID 27 | invalid = [ 28 | true, 29 | false, 30 | Object.new, 31 | ] 32 | 33 | if RUBY_VERSION.to_f >= 3.0 34 | invalid << {} 35 | end 36 | 37 | invalid.each do |v| 38 | assert_raise ArgumentError do 39 | Post.sort_order(v, :asc).limit(1) 40 | end 41 | end 42 | 43 | assert_raise ArgumentError do 44 | Post.sort_order(Object.new, :asc).limit(1) 45 | end 46 | 47 | ### TEST UNIQUE CASES 48 | 49 | if RUBY_VERSION.to_f < 3.0 50 | ### HASH - this is allowed because its treated as keyword arguments 51 | Post.sort_order({}).limit(1) 52 | 53 | assert_raise do 54 | Post.sort_order({}, :desc).limit(1) 55 | end 56 | end 57 | end 58 | 59 | def test_sort_direction_errors 60 | valid = [ 61 | "ASC", 62 | "DESC", 63 | "ASC NULLS FIRST", 64 | "ASC NULLS LAST", 65 | "DESC NULLS FIRST", 66 | "DESC NULLS LAST", 67 | nil, 68 | "", 69 | 70 | ### NASTY BUT TECHNICALLY ALLOWED BECAUSE OF SANITIZATION TECHNIQUE 71 | "ASC NULLS FIRST", 72 | " ASC ", 73 | "ASC\n", 74 | "ASC\tNULLS\tFirst", 75 | ].freeze 76 | 77 | valid.each do |direction| 78 | PostWithBaseOrderA.sort_order("x", direction).limit(1) 79 | 80 | if direction 81 | direction = direction.try!(:downcase) 82 | 83 | PostWithBaseOrderA.sort_order("x", direction).limit(1) 84 | 85 | direction = direction.try!(:to_sym) 86 | 87 | PostWithBaseOrderA.sort_order("foobar", direction).limit(1) 88 | end 89 | end 90 | 91 | invalid = [ 92 | false, 93 | true, 94 | Object.new, 95 | [], 96 | 'ASCC', 97 | ] 98 | 99 | if RUBY_VERSION.to_f >= 3.0 100 | invalid << {} 101 | end 102 | 103 | invalid.each do |direction| 104 | assert_raise ArgumentError do 105 | PostWithBaseOrderA.sort_order("foobar", direction).limit(1) 106 | end 107 | end 108 | 109 | ### TEST UNIQUE CASES 110 | 111 | if RUBY_VERSION.to_f < 3.0 112 | ### HASH - this is allowed because its treated as keyword arguments 113 | Post.sort_order("foobar", {}).limit(1).to_sql.include?("foobar ASC") 114 | 115 | assert_raise do 116 | Post.sort_order("foobar", {}, {}).limit(1) 117 | end 118 | end 119 | end 120 | 121 | def test_argument_base_sort_order_errors 122 | assert_not Post.respond_to?(:base_sort_order) 123 | 124 | valid = [ 125 | nil, 126 | true, 127 | false, 128 | "", 129 | "foobar", 130 | ] 131 | 132 | valid.each do |v| 133 | Post.sort_order(base_sort_order: v).limit(1) 134 | end 135 | 136 | invalid = [ 137 | :foobar, 138 | [], 139 | {}, 140 | Object.new, 141 | ] 142 | 143 | invalid.each do |v| 144 | assert_raise ArgumentError do 145 | Post.sort_order(base_sort_order: v).limit(1) 146 | end 147 | end 148 | end 149 | 150 | def test_class_method_base_sort_order_errors 151 | klass = PostWithVolatileBaseOrder 152 | 153 | assert klass.respond_to?(:base_sort_order) 154 | 155 | valid = [ 156 | nil, 157 | false, 158 | "", 159 | "foobar", 160 | ] 161 | 162 | valid.each do |v| 163 | silence_warnings do 164 | klass.define_singleton_method :base_sort_order do 165 | v 166 | end 167 | end 168 | 169 | if v.nil? 170 | assert_nil klass.base_sort_order 171 | else 172 | assert_equal v, klass.base_sort_order 173 | end 174 | 175 | klass.sort_order.limit(1) 176 | end 177 | 178 | invalid = [ 179 | true, 180 | :foobar, 181 | [], 182 | {}, 183 | Object.new, 184 | ] 185 | 186 | invalid.each do |v| 187 | silence_warnings do 188 | klass.define_singleton_method :base_sort_order do 189 | v 190 | end 191 | end 192 | 193 | if v.nil? 194 | assert_nil klass.base_sort_order 195 | else 196 | assert_equal v, klass.base_sort_order 197 | end 198 | 199 | assert_raise ArgumentError do 200 | klass.sort_order.limit(1) 201 | end 202 | end 203 | end 204 | 205 | end 206 | --------------------------------------------------------------------------------