sortiri 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.rubocop.yml +58 -0
- data/.travis.yml +10 -0
- data/Gemfile +9 -0
- data/Gemfile.lock +72 -0
- data/LICENSE.txt +22 -0
- data/README.md +175 -0
- data/Rakefile +15 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/sortiri.rb +29 -0
- data/lib/sortiri/active_record/column.rb +29 -0
- data/lib/sortiri/active_record/foreign_column.rb +19 -0
- data/lib/sortiri/column.rb +75 -0
- data/lib/sortiri/configuration.rb +12 -0
- data/lib/sortiri/generators/order_by_generator.rb +68 -0
- data/lib/sortiri/missing_column.rb +37 -0
- data/lib/sortiri/model.rb +52 -0
- data/lib/sortiri/parser.rb +30 -0
- data/lib/sortiri/utils.rb +28 -0
- data/lib/sortiri/version.rb +5 -0
- data/lib/sortiri/view_helpers/table_sorter.rb +17 -0
- data/sortiri.gemspec +36 -0
- metadata +167 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 3c6e2f133cbd4f3d2e2be7a72ac383bc8e5e4098a0baa7cc9960fad805bdf9e4
|
4
|
+
data.tar.gz: a85d69d4ced33fa35f2f5f9e5c42b6c2ea8bc62453891020955df2ad92810551
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 3978d8f94465d7c292e91e706c8dda6c65868b31685c3cb13c09a9b59e7835287ce2238db8a075172a56f9ccbb5697df60fa99ea2ddd50e1b10a9777eb70bbb4
|
7
|
+
data.tar.gz: c886e096946103f324a73a65242b500f59db5fc78483b81dd80c26e55402091503c5c1825ffb081a89f0a6c5d674af2173a229c46b537ca04412b3b38d21e3eb
|
data/.gitignore
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
require: rubocop-rake
|
2
|
+
|
3
|
+
AllCops:
|
4
|
+
TargetRubyVersion: 2.6
|
5
|
+
NewCops: enable
|
6
|
+
Exclude:
|
7
|
+
- 'vendor/**/*'
|
8
|
+
- 'test/**/*_test.rb'
|
9
|
+
- '*.md'
|
10
|
+
- 'bin/**'
|
11
|
+
|
12
|
+
Metrics/BlockLength:
|
13
|
+
Max: 36
|
14
|
+
Exclude:
|
15
|
+
- sortiri.gemspec
|
16
|
+
|
17
|
+
Metrics/BlockNesting:
|
18
|
+
Max: 2
|
19
|
+
|
20
|
+
Layout/LineLength:
|
21
|
+
AllowURI: true
|
22
|
+
Enabled: false
|
23
|
+
|
24
|
+
Metrics/MethodLength:
|
25
|
+
CountComments: false
|
26
|
+
Max: 15
|
27
|
+
|
28
|
+
Metrics/ModuleLength:
|
29
|
+
Max: 120
|
30
|
+
|
31
|
+
Metrics/ClassLength:
|
32
|
+
Max: 120
|
33
|
+
|
34
|
+
Metrics/ParameterLists:
|
35
|
+
Max: 5
|
36
|
+
CountKeywordArgs: true
|
37
|
+
|
38
|
+
Style/CollectionMethods:
|
39
|
+
Enabled: true
|
40
|
+
PreferredMethods:
|
41
|
+
collect: 'map'
|
42
|
+
collect!: 'map!'
|
43
|
+
inject: 'reduce'
|
44
|
+
find: 'detect'
|
45
|
+
find_all: 'select'
|
46
|
+
delete: 'gsub'
|
47
|
+
|
48
|
+
Style/Documentation:
|
49
|
+
Enabled: false
|
50
|
+
|
51
|
+
Layout/DotPosition:
|
52
|
+
EnforcedStyle: trailing
|
53
|
+
|
54
|
+
Naming/FileName:
|
55
|
+
Enabled: false
|
56
|
+
|
57
|
+
Layout/AccessModifierIndentation:
|
58
|
+
Enabled: false
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
sortiri (0.1.0)
|
5
|
+
activerecord (>= 4.2)
|
6
|
+
activesupport (>= 4.2)
|
7
|
+
|
8
|
+
GEM
|
9
|
+
remote: https://rubygems.org/
|
10
|
+
specs:
|
11
|
+
activemodel (6.1.0)
|
12
|
+
activesupport (= 6.1.0)
|
13
|
+
activerecord (6.1.0)
|
14
|
+
activemodel (= 6.1.0)
|
15
|
+
activesupport (= 6.1.0)
|
16
|
+
activesupport (6.1.0)
|
17
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
18
|
+
i18n (>= 1.6, < 2)
|
19
|
+
minitest (>= 5.1)
|
20
|
+
tzinfo (~> 2.0)
|
21
|
+
zeitwerk (~> 2.3)
|
22
|
+
ast (2.4.1)
|
23
|
+
concurrent-ruby (1.1.7)
|
24
|
+
i18n (1.8.6)
|
25
|
+
concurrent-ruby (~> 1.0)
|
26
|
+
minitest (5.14.2)
|
27
|
+
mocha (1.12.0)
|
28
|
+
parallel (1.20.1)
|
29
|
+
parser (3.0.0.0)
|
30
|
+
ast (~> 2.4.1)
|
31
|
+
rainbow (3.0.0)
|
32
|
+
rake (12.3.3)
|
33
|
+
regexp_parser (2.0.3)
|
34
|
+
rexml (3.2.4)
|
35
|
+
rubocop (1.7.0)
|
36
|
+
parallel (~> 1.10)
|
37
|
+
parser (>= 2.7.1.5)
|
38
|
+
rainbow (>= 2.2.2, < 4.0)
|
39
|
+
regexp_parser (>= 1.8, < 3.0)
|
40
|
+
rexml
|
41
|
+
rubocop-ast (>= 1.2.0, < 2.0)
|
42
|
+
ruby-progressbar (~> 1.7)
|
43
|
+
unicode-display_width (>= 1.4.0, < 2.0)
|
44
|
+
rubocop-ast (1.4.0)
|
45
|
+
parser (>= 2.7.1.5)
|
46
|
+
rubocop-rake (0.5.1)
|
47
|
+
rubocop
|
48
|
+
ruby-progressbar (1.11.0)
|
49
|
+
sqlite3 (1.4.2)
|
50
|
+
temping (3.10.0)
|
51
|
+
activerecord (>= 4.2)
|
52
|
+
activesupport (>= 4.2)
|
53
|
+
tzinfo (2.0.4)
|
54
|
+
concurrent-ruby (~> 1.0)
|
55
|
+
unicode-display_width (1.7.0)
|
56
|
+
zeitwerk (2.4.2)
|
57
|
+
|
58
|
+
PLATFORMS
|
59
|
+
ruby
|
60
|
+
|
61
|
+
DEPENDENCIES
|
62
|
+
minitest (~> 5.0)
|
63
|
+
mocha (~> 1.12)
|
64
|
+
rake (~> 12.0)
|
65
|
+
rubocop (~> 1.7)
|
66
|
+
rubocop-rake (~> 0.5.1)
|
67
|
+
sortiri!
|
68
|
+
sqlite3 (~> 1.4.0)
|
69
|
+
temping (~> 3.10)
|
70
|
+
|
71
|
+
BUNDLED WITH
|
72
|
+
2.1.4
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2021 Fabrikatör
|
4
|
+
Copyright (c) 2021 H. Can Yıldırım
|
5
|
+
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
8
|
+
in the Software without restriction, including without limitation the rights
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
11
|
+
furnished to do so, subject to the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be included in
|
14
|
+
all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
22
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,175 @@
|
|
1
|
+
# Description
|
2
|
+
|
3
|
+
Sortiri is a clean and lightweight solution for making ActiveRecord::Base objects sortable.
|
4
|
+
|
5
|
+
## Getting started
|
6
|
+
|
7
|
+
Sortiri is supported for ActiveRecord 4.2+ on Ruby 2.6.0 and later.
|
8
|
+
|
9
|
+
In your Gemfile, for the last officially released gem:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
gem 'sortiri'
|
13
|
+
```
|
14
|
+
|
15
|
+
## Usage
|
16
|
+
|
17
|
+
To add Sortiri to an ActiveRecord::Base object, simply include the Sortiri module.
|
18
|
+
|
19
|
+
```ruby
|
20
|
+
class User < ActiveRecord::Base
|
21
|
+
include Sortiri::Model
|
22
|
+
end
|
23
|
+
```
|
24
|
+
|
25
|
+
### Ascending
|
26
|
+
|
27
|
+
Sorting is ascending by default and can be reversed by adding a hyphen (-) to the start of the property name.
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
# GET /users?sort=name
|
31
|
+
|
32
|
+
class User < ActiveRecord::Base
|
33
|
+
include Sortiri::Model
|
34
|
+
|
35
|
+
sortable against: %i[name age weight], default_sort: 'age'
|
36
|
+
end
|
37
|
+
|
38
|
+
# users will be sorted by name and ascending (A -> Z)
|
39
|
+
```
|
40
|
+
|
41
|
+
### Descending
|
42
|
+
|
43
|
+
To sort by descending, simply add a hypen (-) to the start of the property name.
|
44
|
+
|
45
|
+
```ruby
|
46
|
+
# GET /users?sort=-name
|
47
|
+
|
48
|
+
class User < ActiveRecord::Base
|
49
|
+
include Sortiri::Model
|
50
|
+
|
51
|
+
sortable against: %i[name age weight], default_sort: 'age'
|
52
|
+
end
|
53
|
+
|
54
|
+
# users will be sorted by name and descending (Z -> A)
|
55
|
+
```
|
56
|
+
|
57
|
+
### Default sort option
|
58
|
+
|
59
|
+
It is mandatory to provide a default sort option for the model. If nothing is passed, the default sort option will be applied.
|
60
|
+
|
61
|
+
```ruby
|
62
|
+
# GET /users
|
63
|
+
|
64
|
+
class User < ActiveRecord::Base
|
65
|
+
include Sortiri::Model
|
66
|
+
|
67
|
+
sortable against: %i[name age weight], default_sort: '-age'
|
68
|
+
end
|
69
|
+
|
70
|
+
# users will be sorted by age and descending
|
71
|
+
```
|
72
|
+
|
73
|
+
### Sorting through associations
|
74
|
+
|
75
|
+
It is possible to sort columns on associated models.
|
76
|
+
|
77
|
+
You can pass a Hash into the :associated_against option to set up sorting through associations. The keys are the names of the associations and the value works just like an :against option for the other model. Right now, sorting deeper than one association away is not supported.
|
78
|
+
|
79
|
+
```ruby
|
80
|
+
class Company < ActiveRecord::Base
|
81
|
+
has_many :users
|
82
|
+
end
|
83
|
+
|
84
|
+
# GET /users?sort=-company.name
|
85
|
+
|
86
|
+
class User < ActiveRecord::Base
|
87
|
+
include Sortiri::Model
|
88
|
+
|
89
|
+
belongs_to :company
|
90
|
+
|
91
|
+
sortable against: %i[name age weight], associated_against: { company: [:name] }, default_sort: '-age'
|
92
|
+
end
|
93
|
+
|
94
|
+
# users will be sorted by their companies name and descending (Z -> A)
|
95
|
+
```
|
96
|
+
|
97
|
+
### Controller
|
98
|
+
|
99
|
+
#### Sorted
|
100
|
+
|
101
|
+
Allows to specify an order string.
|
102
|
+
|
103
|
+
```ruby
|
104
|
+
class UsersController < ApplicationController
|
105
|
+
def index
|
106
|
+
@users = User.sorted(params[:sort])
|
107
|
+
end
|
108
|
+
end
|
109
|
+
```
|
110
|
+
|
111
|
+
#### Sorted!
|
112
|
+
|
113
|
+
Replaces any existing order defined on the relation with the specified order.
|
114
|
+
|
115
|
+
```ruby
|
116
|
+
class UsersController < ApplicationController
|
117
|
+
def index
|
118
|
+
if params[:sort].present?
|
119
|
+
@users = User.sorted!(params[:sort])
|
120
|
+
else
|
121
|
+
@users = User.sorted(params[:sort])
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
```
|
126
|
+
|
127
|
+
### Sortiri's sort_link helper creates table headers that are sortable links
|
128
|
+
|
129
|
+
To use Sortiri's sort_link helper, simply include the Sortiri module on your ApplicationHelper.
|
130
|
+
|
131
|
+
```ruby
|
132
|
+
module ApplicationHelper
|
133
|
+
include Sortiri::ViewHelpers::TableSorter
|
134
|
+
end
|
135
|
+
```
|
136
|
+
|
137
|
+
```erb
|
138
|
+
<%= sort_link(@users, :name, 'First Name') %>
|
139
|
+
```
|
140
|
+
|
141
|
+
```haml
|
142
|
+
= sort_link @users, :name, 'First Name'
|
143
|
+
```
|
144
|
+
|
145
|
+
Additionally css classes can be passed after the title attribute.
|
146
|
+
|
147
|
+
```erb
|
148
|
+
<%= sort_link(@users, :name, 'First Name', 'd-flex justify-content-center') %>
|
149
|
+
```
|
150
|
+
|
151
|
+
```haml
|
152
|
+
= sort_link @users, :name, 'First Name', 'd-flex justify-content-center'
|
153
|
+
```
|
154
|
+
|
155
|
+
### Configuration
|
156
|
+
|
157
|
+
Sortiri uses default up and down arrows for the view helper. This may be changed by
|
158
|
+
setting them in a Sortiri initializer file (typically `config/initializers/sortiri.rb`):
|
159
|
+
|
160
|
+
```ruby
|
161
|
+
Sortiri.configure do |c|
|
162
|
+
c.up_arrow = 'fas fa-angle-up'
|
163
|
+
c.down_arrow = 'fas fa-angle-down'
|
164
|
+
end
|
165
|
+
```
|
166
|
+
|
167
|
+
## Contributing
|
168
|
+
|
169
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/fabrikatorio/sortiri.
|
170
|
+
|
171
|
+
## License
|
172
|
+
|
173
|
+
Copyright © 2019–2021 [Fabrikatör](https://fabrikator.io).
|
174
|
+
|
175
|
+
Licensed under the MIT license, see [License](https://github.com/fabrikatorio/sortiri/blob/master/LICENSE.txt).
|
data/Rakefile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/gem_tasks'
|
4
|
+
require 'rake/testtask'
|
5
|
+
require 'rubocop/rake_task'
|
6
|
+
|
7
|
+
Rake::TestTask.new(:test) do |t|
|
8
|
+
t.libs << 'test'
|
9
|
+
t.libs << 'lib'
|
10
|
+
t.test_files = FileList['test/**/*_test.rb']
|
11
|
+
end
|
12
|
+
|
13
|
+
RuboCop::RakeTask.new(:rubocop)
|
14
|
+
|
15
|
+
task default: :test
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'sortiri'
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require 'irb'
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/lib/sortiri.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_record'
|
4
|
+
require 'active_support/concern'
|
5
|
+
|
6
|
+
require 'sortiri/version'
|
7
|
+
require 'sortiri/configuration'
|
8
|
+
|
9
|
+
require 'sortiri/active_record/column'
|
10
|
+
require 'sortiri/active_record/foreign_column'
|
11
|
+
require 'sortiri/generators/order_by_generator'
|
12
|
+
require 'sortiri/view_helpers/table_sorter'
|
13
|
+
require 'sortiri/column'
|
14
|
+
require 'sortiri/missing_column'
|
15
|
+
require 'sortiri/model'
|
16
|
+
require 'sortiri/parser'
|
17
|
+
require 'sortiri/utils'
|
18
|
+
|
19
|
+
module Sortiri
|
20
|
+
class << self
|
21
|
+
def configuration
|
22
|
+
@configuration ||= Configuration.new
|
23
|
+
end
|
24
|
+
|
25
|
+
def configure
|
26
|
+
yield(configuration)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sortiri
|
4
|
+
module ActiveRecord
|
5
|
+
class Column
|
6
|
+
attr_reader :name, :association_name
|
7
|
+
|
8
|
+
def initialize(name:, model:, association_name: nil)
|
9
|
+
@name = name.to_s
|
10
|
+
@model = model
|
11
|
+
@association_name = association_name
|
12
|
+
end
|
13
|
+
|
14
|
+
def name_with_table_name
|
15
|
+
[table_name, name].join('.')
|
16
|
+
end
|
17
|
+
|
18
|
+
def matches_with?(column_object)
|
19
|
+
return false unless column_object.is_a?(Sortiri::Column)
|
20
|
+
|
21
|
+
name == column_object.name && association_name.to_s == column_object.association.to_s
|
22
|
+
end
|
23
|
+
|
24
|
+
def table_name
|
25
|
+
@model.table_name
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sortiri
|
4
|
+
module ActiveRecord
|
5
|
+
class ForeignColumn < Column
|
6
|
+
def initialize(name:, model:, association_name:)
|
7
|
+
super(name: name, model: model, association_name: association_name)
|
8
|
+
end
|
9
|
+
|
10
|
+
def foreign_key
|
11
|
+
@model.reflect_on_association(association_name).foreign_key
|
12
|
+
end
|
13
|
+
|
14
|
+
def table_name
|
15
|
+
@model.reflect_on_association(association_name).table_name
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sortiri
|
4
|
+
class Column
|
5
|
+
ASCENDING_SIGN = ''
|
6
|
+
DESCENDING_SIGN = '-'
|
7
|
+
ASCENDING_SQL = 'ASC'
|
8
|
+
DESCENDING_SQL = 'DESC'
|
9
|
+
ASSOCIATION_DELIMITER = '.'
|
10
|
+
|
11
|
+
attr_reader :name_with_sign
|
12
|
+
|
13
|
+
def initialize(column:)
|
14
|
+
@name_with_sign = column.to_s
|
15
|
+
end
|
16
|
+
|
17
|
+
def name
|
18
|
+
raw_name = name_with_sign
|
19
|
+
raw_name = raw_name[1..] if raw_name.start_with?(DESCENDING_SIGN)
|
20
|
+
raw_name = raw_name.split(ASSOCIATION_DELIMITER)[1] if foreign_column?
|
21
|
+
|
22
|
+
raw_name
|
23
|
+
end
|
24
|
+
|
25
|
+
def name_with_association
|
26
|
+
return [association, name].join(ASSOCIATION_DELIMITER) if foreign_column?
|
27
|
+
|
28
|
+
name
|
29
|
+
end
|
30
|
+
|
31
|
+
def association
|
32
|
+
return unless foreign_column?
|
33
|
+
|
34
|
+
# name might have '-' sign, delete_prefix removes that
|
35
|
+
# if no sign in there, it just returns the whole string
|
36
|
+
name_with_sign.delete_prefix(DESCENDING_SIGN).split(ASSOCIATION_DELIMITER).first
|
37
|
+
end
|
38
|
+
|
39
|
+
def direction
|
40
|
+
return DESCENDING_SIGN if name_with_sign.start_with?(DESCENDING_SIGN)
|
41
|
+
|
42
|
+
ASCENDING_SIGN
|
43
|
+
end
|
44
|
+
|
45
|
+
def direction_sql
|
46
|
+
return DESCENDING_SQL if direction == DESCENDING_SIGN
|
47
|
+
|
48
|
+
ASCENDING_SQL
|
49
|
+
end
|
50
|
+
|
51
|
+
def toggle_direction
|
52
|
+
return ASCENDING_SIGN if desc?
|
53
|
+
|
54
|
+
DESCENDING_SIGN
|
55
|
+
end
|
56
|
+
|
57
|
+
def toggle_icon_class
|
58
|
+
return Sortiri.configuration.down_arrow if desc?
|
59
|
+
|
60
|
+
Sortiri.configuration.up_arrow
|
61
|
+
end
|
62
|
+
|
63
|
+
def asc?
|
64
|
+
direction == ASCENDING_SIGN
|
65
|
+
end
|
66
|
+
|
67
|
+
def desc?
|
68
|
+
direction == DESCENDING_SIGN
|
69
|
+
end
|
70
|
+
|
71
|
+
def foreign_column?
|
72
|
+
name_with_sign.include?(ASSOCIATION_DELIMITER)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sortiri
|
4
|
+
module Generators
|
5
|
+
class OrderByGenerator
|
6
|
+
attr_reader :sortable_columns, :whitelisted_columns, :columns_hash, :foreign_columns
|
7
|
+
|
8
|
+
def initialize(sortable_columns:, whitelisted_columns:)
|
9
|
+
@sortable_columns = sortable_columns # array of Sortiri::ActiveRecord::Column
|
10
|
+
@whitelisted_columns = whitelisted_columns # array of Sortiri::Column
|
11
|
+
@columns_hash = map_columns_and_active_record_columns
|
12
|
+
@foreign_columns = select_unique_foreign_columns
|
13
|
+
end
|
14
|
+
|
15
|
+
def sort(model, reorder: false)
|
16
|
+
query = if reorder
|
17
|
+
model.reorder(generate_order_by_clauses)
|
18
|
+
else
|
19
|
+
model.order(generate_order_by_clauses)
|
20
|
+
end
|
21
|
+
query = query.joins(generate_joins(model)) if needs_join?
|
22
|
+
query
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def needs_join?
|
28
|
+
foreign_columns.present?
|
29
|
+
end
|
30
|
+
|
31
|
+
def generate_order_by_clauses
|
32
|
+
columns_hash.map do |column_hash|
|
33
|
+
concatenate_column_and_direction(column_hash)
|
34
|
+
end.join(',')
|
35
|
+
end
|
36
|
+
|
37
|
+
def generate_joins(model)
|
38
|
+
source_arel_table = model.arel_table
|
39
|
+
|
40
|
+
foreign_columns.map do |hash|
|
41
|
+
active_record_column = hash[:active_record_column]
|
42
|
+
target_arel_table = Arel::Table.new(active_record_column.table_name)
|
43
|
+
|
44
|
+
# TODO: find a way to get primary key from target_arel_table
|
45
|
+
source_arel_table.join(target_arel_table, Arel::Nodes::OuterJoin).
|
46
|
+
on(source_arel_table[active_record_column.foreign_key.to_sym].eq(target_arel_table[:id]))
|
47
|
+
end.map(&:join_sources)
|
48
|
+
end
|
49
|
+
|
50
|
+
def concatenate_column_and_direction(column_hash)
|
51
|
+
Arel.sql("#{column_hash[:active_record_column].name_with_table_name} #{column_hash[:column].direction_sql}")
|
52
|
+
end
|
53
|
+
|
54
|
+
def map_columns_and_active_record_columns
|
55
|
+
whitelisted_columns.map do |column|
|
56
|
+
sortable_column = sortable_columns.detect { |c| c.matches_with?(column) }
|
57
|
+
|
58
|
+
{ active_record_column: sortable_column, column: column }
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def select_unique_foreign_columns
|
63
|
+
columns_hash.select { |c| c[:active_record_column].is_a?(Sortiri::ActiveRecord::ForeignColumn) }.
|
64
|
+
uniq { |c| c[:active_record_column].association_name }
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sortiri
|
4
|
+
class MissingColumn < Sortiri::Column
|
5
|
+
def initialize
|
6
|
+
super(column: 'no name')
|
7
|
+
end
|
8
|
+
|
9
|
+
def direction
|
10
|
+
''
|
11
|
+
end
|
12
|
+
|
13
|
+
def direction_sql
|
14
|
+
''
|
15
|
+
end
|
16
|
+
|
17
|
+
def asc?
|
18
|
+
false
|
19
|
+
end
|
20
|
+
|
21
|
+
def desc?
|
22
|
+
false
|
23
|
+
end
|
24
|
+
|
25
|
+
def toggle_direction
|
26
|
+
''
|
27
|
+
end
|
28
|
+
|
29
|
+
def toggle_icon_class
|
30
|
+
''
|
31
|
+
end
|
32
|
+
|
33
|
+
def foreign_column?
|
34
|
+
false
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sortiri
|
4
|
+
module Model
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
attr :sortable_columns, :default_sort
|
9
|
+
|
10
|
+
def sortable(against:, default_sort:, associated_against: {})
|
11
|
+
@sortable_columns ||= []
|
12
|
+
|
13
|
+
columns = against.map do |column|
|
14
|
+
Sortiri::ActiveRecord::Column.new(name: column, model: self)
|
15
|
+
end
|
16
|
+
|
17
|
+
association_columns = associated_against.map do |association_name, columns_array|
|
18
|
+
columns_array.map do |column|
|
19
|
+
Sortiri::ActiveRecord::ForeignColumn.new(name: column, model: self, association_name: association_name)
|
20
|
+
end
|
21
|
+
end.flatten
|
22
|
+
|
23
|
+
@sortable_columns = columns + association_columns
|
24
|
+
@default_sort = default_sort
|
25
|
+
end
|
26
|
+
|
27
|
+
def parse_sorting(sort_query_string)
|
28
|
+
sort_string = (sort_query_string.presence || default_sort)
|
29
|
+
whitelisted_columns = Sortiri::Parser.new(sortable_columns: sortable_columns, sort_string: sort_string).whitelisted_columns
|
30
|
+
Sortiri::Generators::OrderByGenerator.new(sortable_columns: sortable_columns, whitelisted_columns: whitelisted_columns)
|
31
|
+
end
|
32
|
+
|
33
|
+
# sort_query_string is a string seperated by comma and takes - sign
|
34
|
+
# when it's descending. all of the examples below are valid.
|
35
|
+
# request => GET /users?sort=-id,name,email || params => '-id,name,email'
|
36
|
+
# request => GET /users?sort=name,email || params => 'name,email'
|
37
|
+
# request => GET /users || params => nil
|
38
|
+
def sorted(sort_query_string = nil)
|
39
|
+
generator = parse_sorting(sort_query_string)
|
40
|
+
generator.sort(self)
|
41
|
+
end
|
42
|
+
|
43
|
+
# this function will override order by clauses which defined
|
44
|
+
# before sorted! call and force them to use its way.
|
45
|
+
# every example above are still valid for this function.
|
46
|
+
def sorted!(sort_query_string = nil)
|
47
|
+
generator = parse_sorting(sort_query_string)
|
48
|
+
generator.sort(self, reorder: true)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sortiri
|
4
|
+
class Parser
|
5
|
+
DELIMITER = ','
|
6
|
+
|
7
|
+
attr_reader :sortable_columns, :sort_string
|
8
|
+
|
9
|
+
def initialize(sortable_columns:, sort_string:)
|
10
|
+
@sortable_columns = sortable_columns # Array of Sortiri::ActiveRecord::Column
|
11
|
+
@sort_string = sort_string
|
12
|
+
end
|
13
|
+
|
14
|
+
def whitelisted_columns
|
15
|
+
columns = self.class.parse(sort_string: sort_string)
|
16
|
+
|
17
|
+
columns.select do |column|
|
18
|
+
sortable_columns.any? { |c| c.matches_with?(column) }
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.parse(sort_string:)
|
23
|
+
return [] if sort_string.blank?
|
24
|
+
|
25
|
+
sort_string.split(DELIMITER).map do |s|
|
26
|
+
Sortiri::Column.new(column: s)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sortiri
|
4
|
+
class Utils
|
5
|
+
attr_reader :sort_string, :column_name, :column
|
6
|
+
|
7
|
+
def initialize(sort_string:, column_name:)
|
8
|
+
@sort_string = sort_string
|
9
|
+
@column_name = column_name.to_s
|
10
|
+
@column = find_column
|
11
|
+
end
|
12
|
+
|
13
|
+
def direction
|
14
|
+
column.toggle_direction
|
15
|
+
end
|
16
|
+
|
17
|
+
def icon_class
|
18
|
+
column.toggle_icon_class
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def find_column
|
24
|
+
columns = Sortiri::Parser.parse(sort_string: sort_string)
|
25
|
+
columns.detect { |column| column.name_with_association == column_name } || MissingColumn.new
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sortiri
|
4
|
+
module ViewHelpers
|
5
|
+
module TableSorter
|
6
|
+
def sort_link(activerecord_relation_object, column, title, css_class = nil)
|
7
|
+
sort_string = params[:sort].presence || activerecord_relation_object.klass.default_sort
|
8
|
+
sorter = Sortiri::Utils.new(sort_string: sort_string, column_name: column)
|
9
|
+
|
10
|
+
link_to(request.query_parameters.merge({ sort: "#{sorter.direction}#{column}" }), class: css_class) do
|
11
|
+
concat title
|
12
|
+
concat content_tag(:i, '', class: sorter.icon_class)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/sortiri.gemspec
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'lib/sortiri/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = 'sortiri'
|
7
|
+
spec.version = Sortiri::VERSION
|
8
|
+
spec.authors = ['H. Can Yıldırım']
|
9
|
+
spec.email = ['huseyin@fabrikator.io']
|
10
|
+
|
11
|
+
spec.summary = 'Sortiri is a clean and lightweight solution for making ActiveRecord::Base objects sortable.'
|
12
|
+
spec.homepage = 'https://github.com/fabrikatorio/sortiri'
|
13
|
+
spec.license = 'MIT'
|
14
|
+
spec.required_ruby_version = Gem::Requirement.new('>= 2.6.0')
|
15
|
+
|
16
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
17
|
+
spec.metadata['source_code_uri'] = 'https://github.com/fabrikatorio/sortiri'
|
18
|
+
|
19
|
+
# Specify which files should be added to the gem when it is released.
|
20
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
21
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
22
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
23
|
+
end
|
24
|
+
spec.bindir = 'exe'
|
25
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
26
|
+
spec.require_paths = ['lib']
|
27
|
+
|
28
|
+
spec.add_dependency 'activerecord', '>= 4.2'
|
29
|
+
spec.add_dependency 'activesupport', '>= 4.2'
|
30
|
+
|
31
|
+
spec.add_development_dependency 'mocha', '~> 1.12'
|
32
|
+
spec.add_development_dependency 'rubocop', '~> 1.7'
|
33
|
+
spec.add_development_dependency 'rubocop-rake', '~> 0.5.1'
|
34
|
+
spec.add_development_dependency 'sqlite3', '~> 1.4.0'
|
35
|
+
spec.add_development_dependency 'temping', '~> 3.10'
|
36
|
+
end
|
metadata
ADDED
@@ -0,0 +1,167 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: sortiri
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- H. Can Yıldırım
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-01-02 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activerecord
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '4.2'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '4.2'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: activesupport
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '4.2'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '4.2'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: mocha
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.12'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.12'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rubocop
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.7'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '1.7'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rubocop-rake
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 0.5.1
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 0.5.1
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: sqlite3
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 1.4.0
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 1.4.0
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: temping
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '3.10'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '3.10'
|
111
|
+
description:
|
112
|
+
email:
|
113
|
+
- huseyin@fabrikator.io
|
114
|
+
executables: []
|
115
|
+
extensions: []
|
116
|
+
extra_rdoc_files: []
|
117
|
+
files:
|
118
|
+
- ".gitignore"
|
119
|
+
- ".rubocop.yml"
|
120
|
+
- ".travis.yml"
|
121
|
+
- Gemfile
|
122
|
+
- Gemfile.lock
|
123
|
+
- LICENSE.txt
|
124
|
+
- README.md
|
125
|
+
- Rakefile
|
126
|
+
- bin/console
|
127
|
+
- bin/setup
|
128
|
+
- lib/sortiri.rb
|
129
|
+
- lib/sortiri/active_record/column.rb
|
130
|
+
- lib/sortiri/active_record/foreign_column.rb
|
131
|
+
- lib/sortiri/column.rb
|
132
|
+
- lib/sortiri/configuration.rb
|
133
|
+
- lib/sortiri/generators/order_by_generator.rb
|
134
|
+
- lib/sortiri/missing_column.rb
|
135
|
+
- lib/sortiri/model.rb
|
136
|
+
- lib/sortiri/parser.rb
|
137
|
+
- lib/sortiri/utils.rb
|
138
|
+
- lib/sortiri/version.rb
|
139
|
+
- lib/sortiri/view_helpers/table_sorter.rb
|
140
|
+
- sortiri.gemspec
|
141
|
+
homepage: https://github.com/fabrikatorio/sortiri
|
142
|
+
licenses:
|
143
|
+
- MIT
|
144
|
+
metadata:
|
145
|
+
homepage_uri: https://github.com/fabrikatorio/sortiri
|
146
|
+
source_code_uri: https://github.com/fabrikatorio/sortiri
|
147
|
+
post_install_message:
|
148
|
+
rdoc_options: []
|
149
|
+
require_paths:
|
150
|
+
- lib
|
151
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
152
|
+
requirements:
|
153
|
+
- - ">="
|
154
|
+
- !ruby/object:Gem::Version
|
155
|
+
version: 2.6.0
|
156
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
157
|
+
requirements:
|
158
|
+
- - ">="
|
159
|
+
- !ruby/object:Gem::Version
|
160
|
+
version: '0'
|
161
|
+
requirements: []
|
162
|
+
rubygems_version: 3.0.8
|
163
|
+
signing_key:
|
164
|
+
specification_version: 4
|
165
|
+
summary: Sortiri is a clean and lightweight solution for making ActiveRecord::Base
|
166
|
+
objects sortable.
|
167
|
+
test_files: []
|