lexorank 0.1.2 → 0.2.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 +4 -4
- data/LICENSE +1 -1
- data/lib/lexorank/rankable.rb +33 -64
- data/lib/lexorank/ranking.rb +132 -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: 422a0db95be12bd9cffe35d7f549b1ebb71dd5de9751424a3794fd18dc2065c1
|
4
|
+
data.tar.gz: 8d00dd0cb9331d2a5152bb9f68c3ba7a29ac84de4732bc03bf5bc78a946c8a58
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c49a7c38ed0853cdddc5fd1664d085ad08b8b2d91c00e8920da4a19078c6949dd29608bd92013eb31fabd2b1b46191c9ef589fb53629196f0582b53d132ca0f6
|
7
|
+
data.tar.gz: 6e303a53f9185913313d20ef37a9a549b1323c174c98cda5f3533e8bf16e1188ece014fa0fbbf8545b55e040b316f671aa960b99adae3b184ab05e00c6783026
|
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
|
-
# if item is currently in front of the index we just use position otherwise position - 1
|
64
|
-
# if the item has no rank we use position - 1
|
65
|
-
position -= 1 if !self.send(self.class.ranking_column) || collection.map(&:id).index(self.id) > position
|
66
|
-
collection.offset(position).limit(2)
|
67
|
-
end
|
68
|
-
|
69
|
-
rank =
|
70
|
-
if self == after && self.send(self.class.ranking_column).present?
|
71
|
-
self.send(self.class.ranking_column)
|
72
|
-
else
|
73
|
-
value_between(before&.send(self.class.ranking_column), after&.send(self.class.ranking_column))
|
74
|
-
end
|
29
|
+
def move_to_end(&)
|
30
|
+
self.class.lexorank_ranking.move_to(self, :last, &)
|
31
|
+
end
|
75
32
|
|
76
|
-
|
33
|
+
def move_to(position, &)
|
34
|
+
self.class.lexorank_ranking.move_to(self, position, &)
|
77
35
|
end
|
78
36
|
|
79
37
|
def move_to!(position)
|
80
|
-
move_to(position)
|
81
|
-
|
38
|
+
move_to(position) do
|
39
|
+
save
|
40
|
+
end
|
82
41
|
end
|
83
42
|
|
84
43
|
def move_to_top!
|
85
|
-
|
44
|
+
move_to_top do
|
45
|
+
save
|
46
|
+
end
|
86
47
|
end
|
87
48
|
|
88
|
-
|
49
|
+
def move_to_end!
|
50
|
+
move_to_end do
|
51
|
+
save
|
52
|
+
end
|
53
|
+
end
|
89
54
|
|
55
|
+
def no_rank?
|
56
|
+
!send(self.class.lexorank_ranking.field)
|
57
|
+
end
|
58
|
+
end
|
90
59
|
end
|
91
|
-
ActiveRecord::Base.send(:include, Lexorank::Rankable)
|
92
60
|
|
61
|
+
ActiveRecord::Base.include Lexorank::Rankable
|
@@ -0,0 +1,132 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Lexorank::Ranking
|
4
|
+
include Lexorank
|
5
|
+
|
6
|
+
attr_reader :record_class, :original_field, :field, :original_group_by, :group_by, :advisory_lock_config
|
7
|
+
|
8
|
+
def initialize(record_class:, field:, group_by:, advisory_lock:)
|
9
|
+
@record_class = record_class
|
10
|
+
@original_field = field
|
11
|
+
@field = process_column_name(field)
|
12
|
+
@original_group_by = group_by
|
13
|
+
@group_by = process_group_by_column_name(group_by)
|
14
|
+
@advisory_lock_config = { enabled: record_class.respond_to?(:with_advisory_lock) }.merge(advisory_lock)
|
15
|
+
end
|
16
|
+
|
17
|
+
def validate!
|
18
|
+
if advisory_lock_config[:enabled] && !record_class.respond_to?(:with_advisory_lock)
|
19
|
+
raise(
|
20
|
+
Lexorank::InvalidConfigError,
|
21
|
+
"Cannot enable advisory lock if #{record_class.name} does not respond to #with_advisory_lock. " \
|
22
|
+
'Consider installing the with_advisory_lock gem (https://rubygems.org/gems/with_advisory_lock).'
|
23
|
+
)
|
24
|
+
end
|
25
|
+
|
26
|
+
unless @field
|
27
|
+
# TODO: Make this raise an error. Supplying an invalid column should raise.
|
28
|
+
warn "The supplied ranking column \"#{@original_field}\" is not a column of the model!"
|
29
|
+
end
|
30
|
+
|
31
|
+
if original_group_by && !group_by
|
32
|
+
warn "The supplied grouping by \"#{original_group_by}\" is neither a column nor an association of the model!"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def move_to(instance, position)
|
37
|
+
if block_given? && advisory_locks_enabled?
|
38
|
+
return with_lock_if_enabled(instance) do
|
39
|
+
move_to(instance, position)
|
40
|
+
yield
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
collection = record_class.ranked
|
45
|
+
if group_by.present?
|
46
|
+
collection = collection.where("#{group_by}": instance.send(group_by))
|
47
|
+
end
|
48
|
+
|
49
|
+
# exceptions:
|
50
|
+
# move to the beginning (aka move to position 0)
|
51
|
+
# move to end (aka position = collection.size - 1)
|
52
|
+
# when moving to the end of the collection the offset and limit statement automatically handles
|
53
|
+
# that 'after' is nil which is the same like [collection.last, nil]
|
54
|
+
before, after =
|
55
|
+
if position == :last
|
56
|
+
[collection.last, nil]
|
57
|
+
elsif position.zero?
|
58
|
+
[nil, collection.first]
|
59
|
+
else
|
60
|
+
collection.where.not(id: instance.id).offset(position - 1).limit(2)
|
61
|
+
end
|
62
|
+
|
63
|
+
# If position >= collection.size both `before` and `after` will be nil. In this case
|
64
|
+
# we set before to the last element of the collection
|
65
|
+
if before.nil? && after.nil?
|
66
|
+
before = collection.last
|
67
|
+
end
|
68
|
+
|
69
|
+
rank =
|
70
|
+
if (self == after && send(field).present?) || (before == self && after.nil?)
|
71
|
+
send(field)
|
72
|
+
else
|
73
|
+
value_between(before&.send(field), after&.send(field))
|
74
|
+
end
|
75
|
+
|
76
|
+
instance.send(:"#{field}=", rank)
|
77
|
+
|
78
|
+
if block_given?
|
79
|
+
yield
|
80
|
+
else
|
81
|
+
rank
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def with_lock_if_enabled(instance, &)
|
86
|
+
if advisory_locks_enabled?
|
87
|
+
advisory_lock_options = advisory_lock_config.except(:enabled, :lock_name)
|
88
|
+
|
89
|
+
record_class.with_advisory_lock(advisory_lock_name(instance), **advisory_lock_options, &)
|
90
|
+
else
|
91
|
+
yield
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def advisory_lock_name(instance)
|
96
|
+
if advisory_lock_config[:lock_name].present?
|
97
|
+
advisory_lock_config[:lock_name].(instance)
|
98
|
+
else
|
99
|
+
"#{record_class.table_name}_update_#{field}".tap do |name|
|
100
|
+
if group_by.present?
|
101
|
+
name << "_group_#{instance.send(group_by)}"
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def advisory_locks_enabled?
|
108
|
+
record_class.respond_to?(:with_advisory_lock) && advisory_lock_config[:enabled]
|
109
|
+
end
|
110
|
+
|
111
|
+
private
|
112
|
+
|
113
|
+
def process_column_name(name)
|
114
|
+
return unless name
|
115
|
+
|
116
|
+
# This requires an active connection... do we want this?
|
117
|
+
if record_class.columns.map(&:name).include?(name.to_s)
|
118
|
+
name
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def process_group_by_column_name(name)
|
123
|
+
processed_name = process_column_name(name)
|
124
|
+
|
125
|
+
# This requires rank! to be after the specific association
|
126
|
+
if name && !processed_name && (association = record_class.reflect_on_association(name))
|
127
|
+
association.foreign_key.to_sym
|
128
|
+
else
|
129
|
+
processed_name
|
130
|
+
end
|
131
|
+
end
|
132
|
+
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.2.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-16 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: []
|