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.
@@ -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: []