sortiri 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ .DS_Store
@@ -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
@@ -0,0 +1,10 @@
1
+ ---
2
+ language: ruby
3
+ cache: bundler
4
+ rvm:
5
+ - 2.6.6
6
+ before_install:
7
+ - gem install bundler -v 2.1.4
8
+ script:
9
+ - bundle exec rake test
10
+ - bundle exec rubocop --config .rubocop.yml
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ # Specify your gem's dependencies in sortiri.gemspec
6
+ gemspec
7
+
8
+ gem 'minitest', '~> 5.0'
9
+ gem 'rake', '~> 12.0'
@@ -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
@@ -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.
@@ -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).
@@ -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
@@ -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__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -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,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sortiri
4
+ class Configuration
5
+ attr_accessor :up_arrow, :down_arrow
6
+
7
+ def initialize
8
+ @up_arrow = 'fas fa-angle-up'
9
+ @down_arrow = 'fas fa-angle-down'
10
+ end
11
+ end
12
+ 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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sortiri
4
+ VERSION = '0.1.0'
5
+ 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
@@ -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: []