├── .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 |
4 |
5 |
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 |
--------------------------------------------------------------------------------