lexorank 0.1.3 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/LICENSE +1 -1
- data/lib/lexorank/rankable.rb +31 -62
- data/lib/lexorank/ranking.rb +117 -0
- data/lib/lexorank/version.rb +3 -1
- data/lib/lexorank.rb +14 -10
- metadata +16 -109
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 705faccc54d5d09c0ce7e8239d777b79ebe9b8857ea2956c6b7f9378f0b027d6
|
4
|
+
data.tar.gz: b61be364fc9dfffb76947d4ad8ae6462ac8d70490a33262bdb96ac8601e1eb7f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 188136b56717efec3aa8dedcdd9f977506a21677814dbe2d85488fa6f2154abe877f1412a4aaab03c1aae7b8f8badacd1982b2f77b12718893594133e10e10ef
|
7
|
+
data.tar.gz: afd223cb7e8a444cc0428cfab024e89044a9918cfe013972ea65d2c4250f4db70b6b5653d3d5ba989ad548f33f6fda34b6730f3a5053872e0f705f56187d7185
|
data/LICENSE
CHANGED
data/lib/lexorank/rankable.rb
CHANGED
@@ -1,92 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'lexorank'
|
4
|
+
require 'lexorank/ranking'
|
2
5
|
require 'active_support/concern'
|
3
6
|
|
4
7
|
module Lexorank::Rankable
|
5
8
|
extend ActiveSupport::Concern
|
6
9
|
|
7
10
|
module ClassMethods
|
8
|
-
attr_reader :
|
11
|
+
attr_reader :lexorank_ranking
|
9
12
|
|
10
|
-
def rank!(field: :rank, group_by: nil)
|
11
|
-
@
|
12
|
-
|
13
|
-
@ranking_group_by = check_column(group_by)
|
14
|
-
unless @ranking_group_by
|
15
|
-
warn "The supplied grouping by \"#{group_by}\" is neither a column nor an association of the model!"
|
16
|
-
end
|
17
|
-
end
|
13
|
+
def rank!(field: :rank, group_by: nil, advisory_lock: {})
|
14
|
+
@lexorank_ranking = Lexorank::Ranking.new(record_class: self, field: field, group_by: group_by, advisory_lock: advisory_lock)
|
15
|
+
lexorank_ranking.validate!
|
18
16
|
|
19
|
-
if
|
20
|
-
|
21
|
-
|
22
|
-
self.include InstanceMethods
|
23
|
-
else
|
24
|
-
warn "The supplied ranking column \"#{field}\" is not a column of the model!"
|
17
|
+
if lexorank_ranking.field
|
18
|
+
scope :ranked, ->(direction: :asc) { where.not("#{lexorank_ranking.field}": nil).order("#{lexorank_ranking.field}": direction) }
|
19
|
+
include InstanceMethods
|
25
20
|
end
|
26
21
|
end
|
27
|
-
|
28
|
-
private
|
29
|
-
|
30
|
-
def check_column(column_name)
|
31
|
-
return unless column_name
|
32
|
-
# This requires an active connection... do we want this?
|
33
|
-
if self.columns.map(&:name).include?(column_name.to_s)
|
34
|
-
column_name
|
35
|
-
# This requires rank! to be after the specific association
|
36
|
-
elsif (association = self.reflect_on_association(column_name))
|
37
|
-
association.foreign_key.to_sym
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
41
22
|
end
|
42
23
|
|
43
24
|
module InstanceMethods
|
44
|
-
def move_to_top
|
45
|
-
move_to(0)
|
25
|
+
def move_to_top(&)
|
26
|
+
move_to(0, &)
|
46
27
|
end
|
47
28
|
|
48
|
-
def
|
49
|
-
|
50
|
-
|
51
|
-
collection = collection.where("#{self.class.ranking_group_by}": send(self.class.ranking_group_by))
|
52
|
-
end
|
53
|
-
|
54
|
-
# exceptions:
|
55
|
-
# move to the beginning (aka move to position 0)
|
56
|
-
# move to end (aka position = collection.size - 1)
|
57
|
-
# when moving to the end of the collection the offset and limit statement automatically handles
|
58
|
-
# that 'after' is nil which is the same like [collection.last, nil]
|
59
|
-
before, after =
|
60
|
-
if position == 0
|
61
|
-
[nil, collection.first]
|
62
|
-
else
|
63
|
-
collection.where.not(id: self.id).offset(position - 1).limit(2)
|
64
|
-
end
|
65
|
-
|
66
|
-
rank =
|
67
|
-
if self == after && self.send(self.class.ranking_column).present?
|
68
|
-
self.send(self.class.ranking_column)
|
69
|
-
else
|
70
|
-
value_between(before&.send(self.class.ranking_column), after&.send(self.class.ranking_column))
|
71
|
-
end
|
29
|
+
def move_to_end(&)
|
30
|
+
self.class.lexorank_ranking.move_to(self, :last, &)
|
31
|
+
end
|
72
32
|
|
73
|
-
|
33
|
+
def move_to(position, &)
|
34
|
+
self.class.lexorank_ranking.move_to(self, position, &)
|
74
35
|
end
|
75
36
|
|
76
37
|
def move_to!(position)
|
77
|
-
move_to(position)
|
78
|
-
|
38
|
+
move_to(position) do
|
39
|
+
save
|
40
|
+
end
|
79
41
|
end
|
80
42
|
|
81
43
|
def move_to_top!
|
82
|
-
|
44
|
+
move_to_top do
|
45
|
+
save
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def move_to_end!
|
50
|
+
move_to_end do
|
51
|
+
save
|
52
|
+
end
|
83
53
|
end
|
84
54
|
|
85
55
|
def no_rank?
|
86
|
-
!
|
56
|
+
!send(self.class.lexorank_ranking.field)
|
87
57
|
end
|
88
58
|
end
|
89
|
-
|
90
59
|
end
|
91
|
-
ActiveRecord::Base.send(:include, Lexorank::Rankable)
|
92
60
|
|
61
|
+
ActiveRecord::Base.include Lexorank::Rankable
|
@@ -0,0 +1,117 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Lexorank::Ranking
|
4
|
+
include Lexorank
|
5
|
+
|
6
|
+
attr_reader :record_class, :field, :group_by, :advisory_lock_config
|
7
|
+
|
8
|
+
def initialize(record_class:, field:, group_by:, advisory_lock:)
|
9
|
+
@record_class = record_class
|
10
|
+
@field = field
|
11
|
+
@group_by = process_group_by_column_name(group_by)
|
12
|
+
@advisory_lock_config = { enabled: record_class.respond_to?(:with_advisory_lock) }.merge(advisory_lock)
|
13
|
+
end
|
14
|
+
|
15
|
+
def validate!
|
16
|
+
if advisory_lock_config[:enabled] && !record_class.respond_to?(:with_advisory_lock)
|
17
|
+
raise(
|
18
|
+
Lexorank::InvalidConfigError,
|
19
|
+
"Cannot enable advisory lock if #{record_class.name} does not respond to #with_advisory_lock. " \
|
20
|
+
'Consider installing the with_advisory_lock gem (https://rubygems.org/gems/with_advisory_lock).'
|
21
|
+
)
|
22
|
+
end
|
23
|
+
|
24
|
+
unless field
|
25
|
+
raise(
|
26
|
+
Lexorank::InvalidConfigError,
|
27
|
+
'The supplied ":field" option cannot be "nil"!'
|
28
|
+
)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def move_to(instance, position)
|
33
|
+
if block_given? && advisory_locks_enabled?
|
34
|
+
return with_lock_if_enabled(instance) do
|
35
|
+
move_to(instance, position)
|
36
|
+
yield
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
collection = record_class.ranked
|
41
|
+
if group_by.present?
|
42
|
+
collection = collection.where("#{group_by}": instance.send(group_by))
|
43
|
+
end
|
44
|
+
|
45
|
+
# exceptions:
|
46
|
+
# move to the beginning (aka move to position 0)
|
47
|
+
# move to end (aka position = collection.size - 1)
|
48
|
+
# when moving to the end of the collection the offset and limit statement automatically handles
|
49
|
+
# that 'after' is nil which is the same like [collection.last, nil]
|
50
|
+
before, after =
|
51
|
+
if position == :last
|
52
|
+
[collection.last, nil]
|
53
|
+
elsif position.zero?
|
54
|
+
[nil, collection.first]
|
55
|
+
else
|
56
|
+
collection.where.not(id: instance.id).offset(position - 1).limit(2)
|
57
|
+
end
|
58
|
+
|
59
|
+
# If position >= collection.size both `before` and `after` will be nil. In this case
|
60
|
+
# we set before to the last element of the collection
|
61
|
+
if before.nil? && after.nil?
|
62
|
+
before = collection.last
|
63
|
+
end
|
64
|
+
|
65
|
+
rank =
|
66
|
+
if (self == after && send(field).present?) || (before == self && after.nil?)
|
67
|
+
send(field)
|
68
|
+
else
|
69
|
+
value_between(before&.send(field), after&.send(field))
|
70
|
+
end
|
71
|
+
|
72
|
+
instance.send(:"#{field}=", rank)
|
73
|
+
|
74
|
+
if block_given?
|
75
|
+
yield
|
76
|
+
else
|
77
|
+
rank
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def with_lock_if_enabled(instance, &)
|
82
|
+
if advisory_locks_enabled?
|
83
|
+
advisory_lock_options = advisory_lock_config.except(:enabled, :lock_name)
|
84
|
+
|
85
|
+
record_class.with_advisory_lock(advisory_lock_name(instance), **advisory_lock_options, &)
|
86
|
+
else
|
87
|
+
yield
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def advisory_lock_name(instance)
|
92
|
+
if advisory_lock_config[:lock_name].present?
|
93
|
+
advisory_lock_config[:lock_name].(instance)
|
94
|
+
else
|
95
|
+
"#{record_class.table_name}_update_#{field}".tap do |name|
|
96
|
+
if group_by.present?
|
97
|
+
name << "_group_#{instance.send(group_by)}"
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def advisory_locks_enabled?
|
104
|
+
record_class.respond_to?(:with_advisory_lock) && advisory_lock_config[:enabled]
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
|
109
|
+
def process_group_by_column_name(name)
|
110
|
+
# This requires rank! to be after the specific association
|
111
|
+
if name && (association = record_class.reflect_on_association(name))
|
112
|
+
association.foreign_key.to_sym
|
113
|
+
else
|
114
|
+
name
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
data/lib/lexorank/version.rb
CHANGED
data/lib/lexorank.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'lexorank/version'
|
2
4
|
|
3
5
|
# Inspired by https://github.com/DevStarSJ/LexoRank/blob/master/lexo_rank.rb licensed under
|
@@ -24,13 +26,14 @@ require 'lexorank/version'
|
|
24
26
|
# SOFTWARE.
|
25
27
|
module Lexorank
|
26
28
|
class InvalidRankError < StandardError; end
|
29
|
+
class InvalidConfigError < StandardError; end
|
27
30
|
|
28
|
-
MIN_CHAR = '0'
|
29
|
-
MAX_CHAR = 'z'
|
31
|
+
MIN_CHAR = '0'
|
32
|
+
MAX_CHAR = 'z'
|
30
33
|
|
31
|
-
def value_between(
|
32
|
-
before =
|
33
|
-
after =
|
34
|
+
def value_between(before_, after_)
|
35
|
+
before = before_ || MIN_CHAR
|
36
|
+
after = after_ || MAX_CHAR
|
34
37
|
|
35
38
|
rank = ''
|
36
39
|
|
@@ -64,8 +67,10 @@ module Lexorank
|
|
64
67
|
#
|
65
68
|
# Please report if you have another opinion about that or if you reached the exception! (of course you can force it by using `value_between(nil, '0')`)
|
66
69
|
if rank >= after
|
67
|
-
raise InvalidRankError,
|
68
|
-
|
70
|
+
raise InvalidRankError,
|
71
|
+
'This rank should not be achievable using the Lexorank::Rankable module! ' \
|
72
|
+
'Please report to https://github.com/richardboehme/lexorank/issues! ' \
|
73
|
+
"The supplied ranks were #{before_.inspect} and #{after_.inspect}. Please include those in the issue description."
|
69
74
|
end
|
70
75
|
rank
|
71
76
|
end
|
@@ -75,8 +80,7 @@ module Lexorank
|
|
75
80
|
middle_ascii.chr
|
76
81
|
end
|
77
82
|
|
78
|
-
def get_char(
|
79
|
-
|
83
|
+
def get_char(string, index, default_char)
|
84
|
+
index >= string.length ? default_char : string[index]
|
80
85
|
end
|
81
|
-
|
82
86
|
end
|
metadata
CHANGED
@@ -1,29 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: lexorank
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Richard Böhme
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2024-08-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
-
- !ruby/object:Gem::Dependency
|
14
|
-
name: activesupport
|
15
|
-
requirement: !ruby/object:Gem::Requirement
|
16
|
-
requirements:
|
17
|
-
- - ">="
|
18
|
-
- !ruby/object:Gem::Version
|
19
|
-
version: '0'
|
20
|
-
type: :runtime
|
21
|
-
prerelease: false
|
22
|
-
version_requirements: !ruby/object:Gem::Requirement
|
23
|
-
requirements:
|
24
|
-
- - ">="
|
25
|
-
- !ruby/object:Gem::Version
|
26
|
-
version: '0'
|
27
13
|
- !ruby/object:Gem::Dependency
|
28
14
|
name: activerecord
|
29
15
|
requirement: !ruby/object:Gem::Requirement
|
@@ -39,104 +25,20 @@ dependencies:
|
|
39
25
|
- !ruby/object:Gem::Version
|
40
26
|
version: '0'
|
41
27
|
- !ruby/object:Gem::Dependency
|
42
|
-
name:
|
43
|
-
requirement: !ruby/object:Gem::Requirement
|
44
|
-
requirements:
|
45
|
-
- - ">="
|
46
|
-
- !ruby/object:Gem::Version
|
47
|
-
version: '0'
|
48
|
-
type: :development
|
49
|
-
prerelease: false
|
50
|
-
version_requirements: !ruby/object:Gem::Requirement
|
51
|
-
requirements:
|
52
|
-
- - ">="
|
53
|
-
- !ruby/object:Gem::Version
|
54
|
-
version: '0'
|
55
|
-
- !ruby/object:Gem::Dependency
|
56
|
-
name: sqlite3
|
57
|
-
requirement: !ruby/object:Gem::Requirement
|
58
|
-
requirements:
|
59
|
-
- - ">="
|
60
|
-
- !ruby/object:Gem::Version
|
61
|
-
version: '0'
|
62
|
-
type: :development
|
63
|
-
prerelease: false
|
64
|
-
version_requirements: !ruby/object:Gem::Requirement
|
65
|
-
requirements:
|
66
|
-
- - ">="
|
67
|
-
- !ruby/object:Gem::Version
|
68
|
-
version: '0'
|
69
|
-
- !ruby/object:Gem::Dependency
|
70
|
-
name: rake
|
71
|
-
requirement: !ruby/object:Gem::Requirement
|
72
|
-
requirements:
|
73
|
-
- - ">="
|
74
|
-
- !ruby/object:Gem::Version
|
75
|
-
version: '0'
|
76
|
-
type: :development
|
77
|
-
prerelease: false
|
78
|
-
version_requirements: !ruby/object:Gem::Requirement
|
79
|
-
requirements:
|
80
|
-
- - ">="
|
81
|
-
- !ruby/object:Gem::Version
|
82
|
-
version: '0'
|
83
|
-
- !ruby/object:Gem::Dependency
|
84
|
-
name: minitest
|
85
|
-
requirement: !ruby/object:Gem::Requirement
|
86
|
-
requirements:
|
87
|
-
- - ">="
|
88
|
-
- !ruby/object:Gem::Version
|
89
|
-
version: '0'
|
90
|
-
type: :development
|
91
|
-
prerelease: false
|
92
|
-
version_requirements: !ruby/object:Gem::Requirement
|
93
|
-
requirements:
|
94
|
-
- - ">="
|
95
|
-
- !ruby/object:Gem::Version
|
96
|
-
version: '0'
|
97
|
-
- !ruby/object:Gem::Dependency
|
98
|
-
name: minitest-reporters
|
99
|
-
requirement: !ruby/object:Gem::Requirement
|
100
|
-
requirements:
|
101
|
-
- - ">="
|
102
|
-
- !ruby/object:Gem::Version
|
103
|
-
version: '0'
|
104
|
-
type: :development
|
105
|
-
prerelease: false
|
106
|
-
version_requirements: !ruby/object:Gem::Requirement
|
107
|
-
requirements:
|
108
|
-
- - ">="
|
109
|
-
- !ruby/object:Gem::Version
|
110
|
-
version: '0'
|
111
|
-
- !ruby/object:Gem::Dependency
|
112
|
-
name: m
|
113
|
-
requirement: !ruby/object:Gem::Requirement
|
114
|
-
requirements:
|
115
|
-
- - ">="
|
116
|
-
- !ruby/object:Gem::Version
|
117
|
-
version: '0'
|
118
|
-
type: :development
|
119
|
-
prerelease: false
|
120
|
-
version_requirements: !ruby/object:Gem::Requirement
|
121
|
-
requirements:
|
122
|
-
- - ">="
|
123
|
-
- !ruby/object:Gem::Version
|
124
|
-
version: '0'
|
125
|
-
- !ruby/object:Gem::Dependency
|
126
|
-
name: pry
|
28
|
+
name: activesupport
|
127
29
|
requirement: !ruby/object:Gem::Requirement
|
128
30
|
requirements:
|
129
31
|
- - ">="
|
130
32
|
- !ruby/object:Gem::Version
|
131
33
|
version: '0'
|
132
|
-
type: :
|
34
|
+
type: :runtime
|
133
35
|
prerelease: false
|
134
36
|
version_requirements: !ruby/object:Gem::Requirement
|
135
37
|
requirements:
|
136
38
|
- - ">="
|
137
39
|
- !ruby/object:Gem::Version
|
138
40
|
version: '0'
|
139
|
-
description:
|
41
|
+
description:
|
140
42
|
email:
|
141
43
|
- richard.boehme1999@gmail.com
|
142
44
|
executables: []
|
@@ -146,12 +48,17 @@ files:
|
|
146
48
|
- LICENSE
|
147
49
|
- lib/lexorank.rb
|
148
50
|
- lib/lexorank/rankable.rb
|
51
|
+
- lib/lexorank/ranking.rb
|
149
52
|
- lib/lexorank/version.rb
|
150
53
|
homepage: https://github.com/richardboehme/lexorank
|
151
54
|
licenses:
|
152
55
|
- MIT
|
153
|
-
metadata:
|
154
|
-
|
56
|
+
metadata:
|
57
|
+
rubygems_mfa_required: 'true'
|
58
|
+
homepage_uri: https://github.com/richardboehme/lexorank
|
59
|
+
source_code_uri: https://github.com/richardboehme/lexorank
|
60
|
+
changelog_uri: https://github.com/richardboehme/lexorank/blob/main/CHANGELOG.md
|
61
|
+
post_install_message:
|
155
62
|
rdoc_options: []
|
156
63
|
require_paths:
|
157
64
|
- lib
|
@@ -159,15 +66,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
159
66
|
requirements:
|
160
67
|
- - ">="
|
161
68
|
- !ruby/object:Gem::Version
|
162
|
-
version:
|
69
|
+
version: 3.1.0
|
163
70
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
164
71
|
requirements:
|
165
72
|
- - ">="
|
166
73
|
- !ruby/object:Gem::Version
|
167
74
|
version: '0'
|
168
75
|
requirements: []
|
169
|
-
rubygems_version: 3.
|
170
|
-
signing_key:
|
76
|
+
rubygems_version: 3.5.9
|
77
|
+
signing_key:
|
171
78
|
specification_version: 4
|
172
79
|
summary: Store order of your models by using lexicographic sorting.
|
173
80
|
test_files: []
|