rankle 0.0.0.pre
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 +36 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/LICENSE.txt +22 -0
- data/README.md +246 -0
- data/Rakefile +14 -0
- data/features/default_ranking.feature +20 -0
- data/features/get_position.feature +19 -0
- data/features/multi-resource_ranking.feature +37 -0
- data/features/named_ranking.feature +49 -0
- data/features/ranking.feature +24 -0
- data/features/set_position.feature +41 -0
- data/features/step_definitions/fruit_steps.rb +29 -0
- data/features/step_definitions/model_steps.rb +101 -0
- data/features/step_definitions/position_steps.rb +24 -0
- data/features/support/env.rb +15 -0
- data/features/support/factories/fruit.rb +16 -0
- data/features/support/factories/vegetable.rb +36 -0
- data/features/support/models.rb +19 -0
- data/features/support/schema.rb +37 -0
- data/lib/generators/rankle/install_generator.rb +18 -0
- data/lib/generators/rankle/templates/migration.rb +17 -0
- data/lib/rankle.rb +94 -0
- data/lib/rankle/ranker.rb +18 -0
- data/lib/rankle/version.rb +3 -0
- data/rankle.gemspec +30 -0
- metadata +211 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 6c1c94e6dadbdd8d36db346f7767bc847d9402d4
|
4
|
+
data.tar.gz: 5923e83360576985bc983215e94041d6c53d2906
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 513aa6cd74b4a39425e66cd19f5ed7980523dee33b4bdd15ea46d5c8dcfc3836f5c3bec7215db8249daec2cc843192d011fb4a7b2e25c74ba58bb70d38cf48a3
|
7
|
+
data.tar.gz: d244dd513e0a1ff12c339f32d21a9d16bb882b8002ecd452382b7bc389ec217bbd1e84a4bd00166d346a50bf4633d9a4a18aff9d86e28717f04e3385d6d6639f
|
data/.gitignore
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
/.config
|
4
|
+
/coverage/
|
5
|
+
/InstalledFiles
|
6
|
+
/pkg/
|
7
|
+
/spec/reports/
|
8
|
+
/test/tmp/
|
9
|
+
/test/version_tmp/
|
10
|
+
/tmp/
|
11
|
+
.idea
|
12
|
+
*.sqlite3
|
13
|
+
|
14
|
+
## Specific to RubyMotion:
|
15
|
+
.dat*
|
16
|
+
.repl_history
|
17
|
+
build/
|
18
|
+
|
19
|
+
## Documentation cache and generated files:
|
20
|
+
/.yardoc/
|
21
|
+
/_yardoc/
|
22
|
+
/doc/
|
23
|
+
/rdoc/
|
24
|
+
|
25
|
+
## Environment normalisation:
|
26
|
+
/.bundle/
|
27
|
+
/lib/bundler/man/
|
28
|
+
|
29
|
+
# for a library or gem, you might want to ignore these files since the code is
|
30
|
+
# intended to run in multiple environments; otherwise, check them in:
|
31
|
+
Gemfile.lock
|
32
|
+
.ruby-version
|
33
|
+
.ruby-gemset
|
34
|
+
|
35
|
+
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
|
36
|
+
.rvmrc
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2015 wburns84
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
22
|
+
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2015 Wil
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,246 @@
|
|
1
|
+
# Rankle (pre-release)
|
2
|
+
|
3
|
+
Rankle provides multi-resource ranking. It uses a separate join table rather than a resource specific position column.
|
4
|
+
|
5
|
+
** Rankle is currently in a pre-release state. It is not optimized for performance and should not be used in
|
6
|
+
production applications. Future work will be tracked with issues. **
|
7
|
+
|
8
|
+
## Installation
|
9
|
+
|
10
|
+
Add this line to your application's Gemfile:
|
11
|
+
|
12
|
+
```ruby
|
13
|
+
gem 'rankle'
|
14
|
+
```
|
15
|
+
|
16
|
+
And then execute:
|
17
|
+
|
18
|
+
$ bundle
|
19
|
+
|
20
|
+
Or install it yourself as:
|
21
|
+
|
22
|
+
$ gem install rankle
|
23
|
+
|
24
|
+
## Getting Started
|
25
|
+
|
26
|
+
Before you can use Rankle, you'll need to generate the index table. Rankle provides a generator to assist with this:
|
27
|
+
|
28
|
+
$ rails g rankle:install
|
29
|
+
|
30
|
+
The generator only creates the migration file. You'll still need to run the migration:
|
31
|
+
|
32
|
+
$ rake db:migrate
|
33
|
+
|
34
|
+
## Default Behavior
|
35
|
+
|
36
|
+
Simply including Rankle is intended to be ineffectual:
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
class Fruit < ActiveRecord::Base
|
40
|
+
end
|
41
|
+
|
42
|
+
Fruit.all.to_a == Fruit.ranked.to_a # true
|
43
|
+
```
|
44
|
+
|
45
|
+
However, new records will respond to position:
|
46
|
+
|
47
|
+
```ruby
|
48
|
+
apple = Fruit.create!
|
49
|
+
orange = Fruit.create!
|
50
|
+
|
51
|
+
apple.position # 0
|
52
|
+
orange.position # 1
|
53
|
+
```
|
54
|
+
|
55
|
+
The ranked method provides an ordered ActiveRecord::Relation:
|
56
|
+
|
57
|
+
```ruby
|
58
|
+
Fruit.create! name, 'apple'
|
59
|
+
Fruit.create! name, 'orange'
|
60
|
+
|
61
|
+
Fruit.ranked.map(&:name) # ['apple', 'orange']
|
62
|
+
```
|
63
|
+
|
64
|
+
## Simple Usage
|
65
|
+
|
66
|
+
You can assign an explicit ranking in several ways. The position attribute can be set directly:
|
67
|
+
|
68
|
+
```ruby
|
69
|
+
apple.update_attribute :position, 1
|
70
|
+
|
71
|
+
apple.position # 1
|
72
|
+
orange.position # 0
|
73
|
+
|
74
|
+
Fruit.ranked.map(&:name) # ['orange', 'apple']
|
75
|
+
```
|
76
|
+
|
77
|
+
When called with an integer, the rank method will assign the position:
|
78
|
+
|
79
|
+
```ruby
|
80
|
+
apple.rank 0
|
81
|
+
|
82
|
+
apple.position # 0
|
83
|
+
orange.position # 1
|
84
|
+
|
85
|
+
Fruit.ranked.map(&:name) # ['apple', 'orange']
|
86
|
+
```
|
87
|
+
|
88
|
+
You can declare a proc to maintain a functional position ranking:
|
89
|
+
|
90
|
+
```ruby
|
91
|
+
class Fruit < ActiveRecord::Base
|
92
|
+
ranks ->(a, b) { a.name < b.name }
|
93
|
+
end
|
94
|
+
|
95
|
+
Fruit.create! name: 'apple'
|
96
|
+
Fruit.create! name: 'orange'
|
97
|
+
Fruit.create! name: 'banana'
|
98
|
+
|
99
|
+
Fruit.ranked.map(&:name) # ['apple', 'banana', 'orange']
|
100
|
+
```
|
101
|
+
|
102
|
+
## Named Ranking
|
103
|
+
|
104
|
+
Passing a symbol to the rank method with a position will update the position to that named rank:
|
105
|
+
|
106
|
+
```ruby
|
107
|
+
apple = Fruit.create!
|
108
|
+
orange = Fruit.create!
|
109
|
+
|
110
|
+
apple.rank :reverse, 1
|
111
|
+
orange.rank :reverse, 0
|
112
|
+
|
113
|
+
apple.position # 0
|
114
|
+
orange.position # 1
|
115
|
+
|
116
|
+
apple.position :reverse # 1
|
117
|
+
orange.position :reverse # 0
|
118
|
+
|
119
|
+
Fruit.ranked.map(&:name) # ['apple', 'orange']
|
120
|
+
Fruit.ranked(:reverse).map(&:name) # ['orange', 'apple']
|
121
|
+
```
|
122
|
+
|
123
|
+
Since positions are not stored with an absolute value, the available positions increases by 1 with each call to the rank method:
|
124
|
+
|
125
|
+
```ruby
|
126
|
+
apple = Fruit.create!
|
127
|
+
banana = Fruit.create!
|
128
|
+
orange = Fruit.create!
|
129
|
+
|
130
|
+
apple.rank :reverse, 2 # [apple]
|
131
|
+
banana.rank :reverse, 1 # [banana, apple]
|
132
|
+
orange.rank :reverse, 0 # [orange, banana, apple]
|
133
|
+
|
134
|
+
apple.position # 0
|
135
|
+
banana.position # 1
|
136
|
+
orange.position # 2
|
137
|
+
|
138
|
+
apple.position :reverse # 1
|
139
|
+
banana.position :reverse # 2
|
140
|
+
orange.position :reverse # 0
|
141
|
+
|
142
|
+
Fruit.ranked.map(&:name) # ['apple', 'banana', 'orange']
|
143
|
+
Fruit.ranked(:reverse).map(&:name) # ['orange', 'apple', 'banana']
|
144
|
+
```
|
145
|
+
|
146
|
+
You can bypass this issue by registering the ranking on the class:
|
147
|
+
|
148
|
+
```ruby
|
149
|
+
class Fruit < ActiveRecord::Base
|
150
|
+
ranks :reverse
|
151
|
+
end
|
152
|
+
|
153
|
+
apple = Fruit.create!
|
154
|
+
banana = Fruit.create!
|
155
|
+
orange = Fruit.create!
|
156
|
+
|
157
|
+
apple.position # 0
|
158
|
+
banana.position # 1
|
159
|
+
orange.position # 2
|
160
|
+
|
161
|
+
apple.position :reverse # 0
|
162
|
+
banana.position :reverse # 1
|
163
|
+
orange.position :reverse # 2
|
164
|
+
|
165
|
+
apple.rank :reverse, 2 # [banana, orange, apple]
|
166
|
+
banana.rank :reverse, 1 # [banana, orange, apple]
|
167
|
+
orange.rank :reverse, 0 # [orange, banana, apple]
|
168
|
+
|
169
|
+
apple.position # 0
|
170
|
+
banana.position # 1
|
171
|
+
orange.position # 2
|
172
|
+
|
173
|
+
apple.position :reverse # 2
|
174
|
+
banana.position :reverse # 1
|
175
|
+
orange.position :reverse # 0
|
176
|
+
|
177
|
+
Fruit.ranked.map(&:name) # ['apple', 'banana', 'orange']
|
178
|
+
Fruit.ranked(:reverse).map(&:name) # ['orange', 'banana', 'apple']
|
179
|
+
```
|
180
|
+
|
181
|
+
## Multiple Resources
|
182
|
+
|
183
|
+
Passing a symbol to the rank method with a position will update the position to that named rank:
|
184
|
+
|
185
|
+
```ruby
|
186
|
+
class Fruit < ActiveRecord::Base
|
187
|
+
end
|
188
|
+
|
189
|
+
class Vegetable < ActiveRecord::Base
|
190
|
+
end
|
191
|
+
|
192
|
+
apple = Fruit.create!
|
193
|
+
carrot = Vegetable.create!
|
194
|
+
|
195
|
+
apple.rank :produce, 0
|
196
|
+
carrot.rank :produce, 1
|
197
|
+
|
198
|
+
apple.position # 0
|
199
|
+
carrot.position # 0
|
200
|
+
|
201
|
+
apple.position :produce # 0
|
202
|
+
carrot.position :produce # 1
|
203
|
+
|
204
|
+
Fruit.ranked.map(&:name) # ['apple']
|
205
|
+
Vegetable.ranked.map(&:name) # ['carrot']
|
206
|
+
|
207
|
+
Fruit.ranked(:produce).map(&:name) # ['apple']
|
208
|
+
Vegetable.ranked(:produce).map(&:name) # ['carrot']
|
209
|
+
```
|
210
|
+
|
211
|
+
Notice that the ranked method can't increase the scope of your query. Multi-resource relations can be accessed through
|
212
|
+
the Rankle class which serves as the global ranking scope.
|
213
|
+
|
214
|
+
```ruby
|
215
|
+
Rankle.ranked(:produce).map(&:name) # ['apple', 'carrot']
|
216
|
+
```
|
217
|
+
|
218
|
+
Passing a symbol to the ranks method will register the resource to that ranking. This will automatically assign the
|
219
|
+
default position to new records within the shared ranking:
|
220
|
+
|
221
|
+
```ruby
|
222
|
+
class Fruit
|
223
|
+
ranks :produce
|
224
|
+
end
|
225
|
+
|
226
|
+
Class Vegetable
|
227
|
+
ranks :produce
|
228
|
+
end
|
229
|
+
|
230
|
+
apple = Fruit.create!
|
231
|
+
carrot = Vegetable.create!
|
232
|
+
|
233
|
+
apple.position # 0
|
234
|
+
carrot.position # 0
|
235
|
+
|
236
|
+
apple.position :produce # 0
|
237
|
+
carrot.position :produce # 1
|
238
|
+
```
|
239
|
+
|
240
|
+
## Contributing
|
241
|
+
|
242
|
+
1. Fork it ( https://github.com/[my-github-username]/rankle/fork )
|
243
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
244
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
245
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
246
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
2
|
+
require 'cucumber'
|
3
|
+
require 'cucumber/rake/task'
|
4
|
+
require 'yard'
|
5
|
+
|
6
|
+
Cucumber::Rake::Task.new(:features) do |t|
|
7
|
+
t.cucumber_opts = 'features --format pretty'
|
8
|
+
end
|
9
|
+
|
10
|
+
YARD::Rake::YardocTask.new do |t|
|
11
|
+
t.files = ['lib/rankle.rb']
|
12
|
+
end
|
13
|
+
|
14
|
+
task :default => [:features, :yard]
|
@@ -0,0 +1,20 @@
|
|
1
|
+
Feature: Default ranking
|
2
|
+
In order to not surprise new customers
|
3
|
+
As a consumer of rankle
|
4
|
+
I want the default behavior to match existing behavior
|
5
|
+
|
6
|
+
Scenario: Empty fruit model
|
7
|
+
Given an empty fruit model
|
8
|
+
Then ranking all has no effect
|
9
|
+
And the ranked fruit array is []
|
10
|
+
|
11
|
+
Scenario: Fruit model with several fruits
|
12
|
+
Given several fruits
|
13
|
+
Then ranking all has no effect
|
14
|
+
And the ranked fruit array is [Apple, Apricot, Banana, Bilberry, Blackberry, Blackcurrant, Blueberry, Boysenberry, Cantaloupe, Currant]
|
15
|
+
|
16
|
+
Scenario: Ranked fruit
|
17
|
+
Given an empty fruit model
|
18
|
+
And an apple
|
19
|
+
And an orange
|
20
|
+
Then the ranked fruit array is [apple, orange]
|
@@ -0,0 +1,19 @@
|
|
1
|
+
Feature: Get position
|
2
|
+
In order to inspect a ranking
|
3
|
+
As a developer
|
4
|
+
I want to retrieve an element's position
|
5
|
+
|
6
|
+
Scenario: Default ranking
|
7
|
+
Given an apple
|
8
|
+
And an orange
|
9
|
+
Then the apple is in position 0
|
10
|
+
And the orange is in position 1
|
11
|
+
And the ranked fruit array is [apple, orange]
|
12
|
+
|
13
|
+
Scenario: Custom ranking
|
14
|
+
Given an apple
|
15
|
+
And an orange
|
16
|
+
When I assign the apple's position to 1
|
17
|
+
Then the apple is in position 1
|
18
|
+
And the orange is in position 0
|
19
|
+
And the ranked fruit array is [orange, apple]
|
@@ -0,0 +1,37 @@
|
|
1
|
+
Feature: Multi-resource ranking
|
2
|
+
In order to rank multiple resources
|
3
|
+
As a consumer of rankle
|
4
|
+
I want to create a multi-resource ranking
|
5
|
+
|
6
|
+
Scenario: Basic ranking
|
7
|
+
Given a 'fruit' model
|
8
|
+
And a 'vegetable' model
|
9
|
+
And an 'apple' fruit
|
10
|
+
And a 'carrot' vegetable
|
11
|
+
When I assign the 'apple' fruit's 'produce' rank to '0'
|
12
|
+
And I assign the 'carrot' vegetable's 'produce' rank to '1'
|
13
|
+
Then the 'apple' fruit's 'default' rank is '0'
|
14
|
+
And the 'carrot' vegetable's 'default' rank is '0'
|
15
|
+
And the 'apple' fruit's 'produce' rank is '0'
|
16
|
+
And the 'carrot' vegetable's 'produce' rank is '1'
|
17
|
+
And the default ranked fruit array is [apple]
|
18
|
+
And the default ranked vegetable array is [carrot]
|
19
|
+
And the produce ranked fruit array is [apple]
|
20
|
+
And the produce ranked vegetable array is [carrot]
|
21
|
+
And the produce ranked rankle array is [apple, carrot]
|
22
|
+
|
23
|
+
|
24
|
+
Scenario: Default ranking
|
25
|
+
Given a 'fruit' model with a 'produce' ranking
|
26
|
+
And a 'vegetable' model with a 'produce' ranking
|
27
|
+
And an 'apple' fruit
|
28
|
+
And a 'carrot' vegetable
|
29
|
+
Then the 'apple' fruit's 'default' rank is '0'
|
30
|
+
And the 'carrot' vegetable's 'default' rank is '0'
|
31
|
+
And the 'apple' fruit's 'produce' rank is '0'
|
32
|
+
And the 'carrot' vegetable's 'produce' rank is '1'
|
33
|
+
And the default ranked fruit array is [apple]
|
34
|
+
And the default ranked vegetable array is [carrot]
|
35
|
+
And the produce ranked fruit array is [apple]
|
36
|
+
And the produce ranked vegetable array is [carrot]
|
37
|
+
And the produce ranked rankle array is [apple, carrot]
|
@@ -0,0 +1,49 @@
|
|
1
|
+
Feature: Named ranking
|
2
|
+
In order to maintain multiple rankings on a single class
|
3
|
+
As a developer
|
4
|
+
I want to create named rankings
|
5
|
+
|
6
|
+
Scenario: Reverse ranking
|
7
|
+
Given an apple
|
8
|
+
And an orange
|
9
|
+
When I assign the 'apple' fruit's 'reverse' rank to '1'
|
10
|
+
When I assign the 'orange' fruit's 'reverse' rank to '0'
|
11
|
+
Then the 'apple' fruit's 'default' rank is '0'
|
12
|
+
And the 'orange' fruit's 'default' rank is '1'
|
13
|
+
And the 'apple' fruit's 'reverse' rank is '1'
|
14
|
+
And the 'orange' fruit's 'reverse' rank is '0'
|
15
|
+
And the default ranked fruit array is [apple, orange]
|
16
|
+
And the reverse ranked fruit array is [orange, apple]
|
17
|
+
|
18
|
+
Scenario: Growing ranking
|
19
|
+
Given an apple
|
20
|
+
And a banana
|
21
|
+
And an orange
|
22
|
+
When I assign the 'apple' fruit's 'reverse' rank to '2'
|
23
|
+
And I assign the 'banana' fruit's 'reverse' rank to '1'
|
24
|
+
And I assign the 'orange' fruit's 'reverse' rank to '0'
|
25
|
+
Then the 'apple' fruit's 'default' rank is '0'
|
26
|
+
And the 'banana' fruit's 'default' rank is '1'
|
27
|
+
And the 'orange' fruit's 'default' rank is '2'
|
28
|
+
And the 'apple' fruit's 'reverse' rank is '1'
|
29
|
+
And the 'banana' fruit's 'reverse' rank is '2'
|
30
|
+
And the 'orange' fruit's 'reverse' rank is '0'
|
31
|
+
And the default ranked fruit array is [apple, banana, orange]
|
32
|
+
And the reverse ranked fruit array is [orange, apple, banana]
|
33
|
+
|
34
|
+
Scenario: Registered ranking
|
35
|
+
Given a 'fruit' class with a 'reverse' ranking
|
36
|
+
And an apple
|
37
|
+
And a banana
|
38
|
+
And an orange
|
39
|
+
When I assign the 'apple' fruit's 'reverse' rank to '2'
|
40
|
+
And I assign the 'banana' fruit's 'reverse' rank to '1'
|
41
|
+
And I assign the 'orange' fruit's 'reverse' rank to '0'
|
42
|
+
Then the 'apple' fruit's 'default' rank is '0'
|
43
|
+
And the 'banana' fruit's 'default' rank is '1'
|
44
|
+
And the 'orange' fruit's 'default' rank is '2'
|
45
|
+
And the 'apple' fruit's 'reverse' rank is '2'
|
46
|
+
And the 'banana' fruit's 'reverse' rank is '1'
|
47
|
+
And the 'orange' fruit's 'reverse' rank is '0'
|
48
|
+
And the default ranked fruit array is [apple, banana, orange]
|
49
|
+
And the reverse ranked fruit array is [orange, banana, apple]
|
@@ -0,0 +1,24 @@
|
|
1
|
+
Feature: Ranks
|
2
|
+
In order to provide an explicit ordering
|
3
|
+
As a developer
|
4
|
+
I want to rank on position
|
5
|
+
|
6
|
+
Scenario: Reverse ranking
|
7
|
+
Given 10 rows
|
8
|
+
When I rank them in reverse order
|
9
|
+
Then ranking is equivalent to all reversed
|
10
|
+
|
11
|
+
Scenario: Update ranking
|
12
|
+
Given 10 rows in default order
|
13
|
+
When I move row 9 to row 0
|
14
|
+
Then ranking is equivalent to all rotated -1
|
15
|
+
|
16
|
+
Scenario: Negative rank
|
17
|
+
Given 10 rows in default order
|
18
|
+
When I move row 9 to row -10
|
19
|
+
Then row 9 is in position 0
|
20
|
+
|
21
|
+
Scenario: Out-of-bounds rank
|
22
|
+
Given 10 rows in default order
|
23
|
+
When I move row 0 to row 30
|
24
|
+
Then row 0 is in position 9
|
@@ -0,0 +1,41 @@
|
|
1
|
+
Feature: Set position
|
2
|
+
In order to assign a ranking
|
3
|
+
As a developer
|
4
|
+
I want to set an element's position
|
5
|
+
|
6
|
+
Scenario: Update position attribute
|
7
|
+
Given an empty fruit model
|
8
|
+
And an 'apple' fruit
|
9
|
+
And an 'orange' fruit
|
10
|
+
When I update the apple's position attribute to 1
|
11
|
+
Then the apple is in position 1
|
12
|
+
And the orange is in position 0
|
13
|
+
Then the ranked fruit array is [orange, apple]
|
14
|
+
|
15
|
+
Scenario: Update position attribute with rank method
|
16
|
+
Given an apple
|
17
|
+
And an orange
|
18
|
+
When I assign the apple's rank to 1
|
19
|
+
Then the apple is in position 1
|
20
|
+
And the orange is in position 0
|
21
|
+
Then the ranked fruit array is [orange, apple]
|
22
|
+
|
23
|
+
|
24
|
+
Scenario: Override default with stabby proc
|
25
|
+
Given a fruit class with a reverse alphabetical default ranking on name
|
26
|
+
And an apple
|
27
|
+
And an orange
|
28
|
+
Then the apple is in position 1
|
29
|
+
And the orange is in position 0
|
30
|
+
Then the ranked fruit array is [orange, apple]
|
31
|
+
|
32
|
+
|
33
|
+
Scenario: Override default with stabby proc (documentation)
|
34
|
+
Given a fruit class with an alphabetical default ranking on name
|
35
|
+
And an apple
|
36
|
+
And an orange
|
37
|
+
And a banana
|
38
|
+
Then the apple is in position 0
|
39
|
+
And the banana is in position 1
|
40
|
+
And the orange is in position 2
|
41
|
+
Then the ranked fruit array is [apple, banana, orange]
|
@@ -0,0 +1,29 @@
|
|
1
|
+
Given(/^an apple$/) do
|
2
|
+
DatabaseCleaner.clean
|
3
|
+
@fruit ||= {}
|
4
|
+
@fruit[:apple] = create :fruit, name: 'apple'
|
5
|
+
end
|
6
|
+
|
7
|
+
Given(/^an orange$/) do
|
8
|
+
@fruit[:orange] = create :fruit, name: 'orange'
|
9
|
+
end
|
10
|
+
|
11
|
+
Given(/^a banana$/) do
|
12
|
+
@fruit[:banana] = create :fruit, name: 'banana'
|
13
|
+
end
|
14
|
+
|
15
|
+
Then(/^the apple is in position (\d+)$/) do |position|
|
16
|
+
expect(@fruit[:apple].position).to eq(position.to_i)
|
17
|
+
end
|
18
|
+
|
19
|
+
Then(/^the orange is in position (\d+)$/) do |position|
|
20
|
+
expect(@fruit[:orange].position).to eq(position.to_i)
|
21
|
+
end
|
22
|
+
|
23
|
+
Then(/^the banana is in position (\d+)$/) do |position|
|
24
|
+
expect(@fruit[:banana].position).to eq(position.to_i)
|
25
|
+
end
|
26
|
+
|
27
|
+
When(/^I assign the apple's position to (\d+)$/) do |position|
|
28
|
+
@fruit[:apple].update_attribute(:position, position.to_i)
|
29
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
Given(/^a '(.*)' model$/) do |name|
|
2
|
+
DatabaseCleaner.clean
|
3
|
+
name.classify.constantize.delete_all
|
4
|
+
end
|
5
|
+
|
6
|
+
Given(/^a '(.*)' model with a '(.*)' ranking$/) do |klass, ranking|
|
7
|
+
step "a '#{klass}' model"
|
8
|
+
klass.classify.constantize.send :ranks, ranking.to_sym
|
9
|
+
end
|
10
|
+
|
11
|
+
|
12
|
+
Given(/^an '(.*)' fruit$/) do |name|
|
13
|
+
@fruit ||= {}
|
14
|
+
@fruit[name.to_sym] = create :fruit, name: name
|
15
|
+
end
|
16
|
+
|
17
|
+
Given(/^a '(.*)' vegetable$/) do |name|
|
18
|
+
@vegetable ||= {}
|
19
|
+
@vegetable[name.to_sym] = create :vegetable, name: name
|
20
|
+
end
|
21
|
+
|
22
|
+
Given(/^an empty (.+) model$/) do |klass|
|
23
|
+
DatabaseCleaner.clean
|
24
|
+
klass.classify.constantize.delete_all
|
25
|
+
end
|
26
|
+
|
27
|
+
Given(/^several fruits$/) do
|
28
|
+
DatabaseCleaner.clean
|
29
|
+
@fruit ||= {}
|
30
|
+
10.times.each do |index|
|
31
|
+
fruit = create :fruit
|
32
|
+
@fruit[fruit.name.to_sym] = fruit
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
Given(/^several points$/) do
|
37
|
+
DatabaseCleaner.clean
|
38
|
+
10.times.each { |index| Point.create!(x: index, y: index) }
|
39
|
+
end
|
40
|
+
|
41
|
+
Given(/^(\d+) rows$/) do |count|
|
42
|
+
DatabaseCleaner.clean
|
43
|
+
count.to_i.times.each { |index| Row.create!(text: index) }
|
44
|
+
end
|
45
|
+
|
46
|
+
Given(/^(\d+) rows in default order$/) do |count|
|
47
|
+
DatabaseCleaner.clean
|
48
|
+
count.to_i.times.each { |index| Row.create!(text: index).update_attribute(:position, index) }
|
49
|
+
end
|
50
|
+
|
51
|
+
Given(/^(\d+) even and odd rows$/) do |count|
|
52
|
+
DatabaseCleaner.clean
|
53
|
+
count.to_i.times.each { |index| Row.create!(text: index.even? ? 'even' : 'odd').update_attribute(:position, index) }
|
54
|
+
end
|
55
|
+
|
56
|
+
When(/^I rank them in reverse order$/) do
|
57
|
+
Row.all.reverse.each_with_index { |row, index| row.update_attribute(:position, index) }
|
58
|
+
end
|
59
|
+
|
60
|
+
When(/^I move row (\d+) to row (-?\d+)$/) do |start_position, end_position|
|
61
|
+
Row.all[start_position.to_i].update_attribute(:position, end_position.to_i)
|
62
|
+
end
|
63
|
+
|
64
|
+
When(/^I reverse rank the even rows$/) do
|
65
|
+
Row.rank(:even)
|
66
|
+
end
|
67
|
+
|
68
|
+
Then(/^ranking is equivalent to all reversed$/) do
|
69
|
+
expect(Row.ranked.to_a).to eq(Row.all.to_a.reverse)
|
70
|
+
end
|
71
|
+
|
72
|
+
Then(/^ranking is equivalent to all rotated (\-\d+)$/) do |positions|
|
73
|
+
expect(Row.ranked.to_a).to eq(Row.all.to_a.rotate(positions.to_i))
|
74
|
+
end
|
75
|
+
|
76
|
+
Then(/^ranking all has no effect$/) do
|
77
|
+
expect(Point.ranked.to_a).to eq(Point.all.to_a)
|
78
|
+
end
|
79
|
+
|
80
|
+
Then(/^row (\d+) is in position (\d+)$/) do |row, position|
|
81
|
+
expect(Row.ranked[position.to_i].id).to eq(row.to_i + 1)
|
82
|
+
end
|
83
|
+
|
84
|
+
Given(/^a fruit class with an alphabetical default ranking on name$/) do
|
85
|
+
Fruit.send :ranks, ->(a, b) { a.name < b.name }
|
86
|
+
end
|
87
|
+
|
88
|
+
Given(/^a fruit class with a reverse alphabetical default ranking on name$/) do
|
89
|
+
Fruit.send :ranks, ->(a, b) { a.name > b.name }
|
90
|
+
end
|
91
|
+
|
92
|
+
Given(/^a 'fruit' class with a 'reverse' ranking$/) do
|
93
|
+
Fruit.send :ranks, :reverse
|
94
|
+
end
|
95
|
+
|
96
|
+
Then(/^the (.*)ranked (.*) array is \[(.*)\]$/) do |ranker, klass, names|
|
97
|
+
ranker = 'default' if ranker.blank?
|
98
|
+
expect(klass.classify.constantize.ranked(ranker.strip.to_sym).map(&:name)).to eq(names.split(',').map do |name|
|
99
|
+
@fruit[name.strip.to_sym].name rescue @vegetable[name.strip.to_sym].name
|
100
|
+
end)
|
101
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
When(/^I update the apple's position attribute to (\d+)$/) do |position|
|
2
|
+
@fruit[:apple].update_attribute(:position, position.to_i)
|
3
|
+
end
|
4
|
+
|
5
|
+
When(/^I assign the apple's rank to (\d+)$/) do |position|
|
6
|
+
@fruit[:apple].rank position.to_i
|
7
|
+
end
|
8
|
+
|
9
|
+
When(/^I assign the '(.*)' fruit's '(.*)' rank to '(\d+)'$/) do |type, name, position|
|
10
|
+
@fruit[type.to_sym].rank name.to_sym, position.to_i
|
11
|
+
end
|
12
|
+
|
13
|
+
When(/^I assign the '(.*)' vegetable's '(.*)' rank to '(\d+)'$/) do |type, name, position|
|
14
|
+
@vegetable[type.to_sym].rank name.to_sym, position.to_i
|
15
|
+
end
|
16
|
+
|
17
|
+
|
18
|
+
Then(/^the '(.*)' fruit's '(.*)' rank is '(\d+)'$/) do |type, name, position|
|
19
|
+
expect(@fruit[type.to_sym].position name).to eq(position.to_i)
|
20
|
+
end
|
21
|
+
|
22
|
+
Then(/^the '(.*)' vegetable's '(.*)' rank is '(\d+)'$/) do |type, name, position|
|
23
|
+
expect(@vegetable[type.to_sym].position name).to eq(position.to_i)
|
24
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'rankle'
|
2
|
+
require 'database_cleaner'
|
3
|
+
require 'factory_girl'
|
4
|
+
|
5
|
+
DatabaseCleaner.strategy = :truncation
|
6
|
+
|
7
|
+
ActiveRecord::Base.establish_connection(
|
8
|
+
adapter: 'sqlite3',
|
9
|
+
database: 'rankle.sqlite3'
|
10
|
+
)
|
11
|
+
|
12
|
+
load File.dirname(__FILE__) + '/schema.rb'
|
13
|
+
load File.dirname(__FILE__) + '/models.rb'
|
14
|
+
|
15
|
+
World FactoryGirl::Syntax::Methods
|
@@ -0,0 +1,16 @@
|
|
1
|
+
FactoryGirl.define do
|
2
|
+
factory :fruit do
|
3
|
+
sequence(:name) { |n| FRUITS[n-1] }
|
4
|
+
end
|
5
|
+
end
|
6
|
+
|
7
|
+
FRUITS = ['Apple', 'Apricot', 'Banana', 'Bilberry', 'Blackberry', 'Blackcurrant', 'Blueberry', 'Boysenberry',
|
8
|
+
'Cantaloupe', 'Currant', 'Cherry', 'Cherimoya', 'Cloudberry', 'Coconut', 'Cranberry', 'Damson', 'Date',
|
9
|
+
'Dragonfruit', 'Durian', 'Elderberry', 'Feijoa', 'Fig', 'Goji berry', 'Gooseberry', 'Grape', 'Raisin',
|
10
|
+
'Grapefruit', 'Guava', 'Huckleberry', 'Jackfruit', 'Jambul', 'Jujube', 'Kiwi fruit', 'Kumquat', 'Lemon',
|
11
|
+
'Lime', 'Loquat', 'Lychee', 'Mango', 'Marion berry', 'Melon', 'Cantaloupe', 'Honeydew', 'Watermelon',
|
12
|
+
'Rock melon', 'Miracle fruit', 'Mulberry', 'Nectarine', 'Olive', 'Orange', 'Clementine', 'Mandarine',
|
13
|
+
'Tangerine', 'Papaya', 'Passionfruit', 'Peach', 'Pear', 'Williams pear', 'Bartlett pear', 'Persimmon',
|
14
|
+
'Physalis', 'Plum/prune (dried plum)', 'Pineapple', 'Pomegranate', 'Pomelo', 'Purple Mangosteen', 'Quince',
|
15
|
+
'Raspberry', 'Salmon berry', 'Black raspberry', 'Rambutan', 'Redcurrant', 'Salal berry', 'Satsuma',
|
16
|
+
'Star fruit', 'Strawberry', 'Tamarillo', 'Ugli fruit']
|
@@ -0,0 +1,36 @@
|
|
1
|
+
FactoryGirl.define do
|
2
|
+
factory :vegetable do
|
3
|
+
sequence(:name) { |n| VEGETABLES[n-1] }
|
4
|
+
end
|
5
|
+
|
6
|
+
#factory :legume, class: Vegetable do
|
7
|
+
# sequence(:name) { |n| LEGUMES[n-1] }
|
8
|
+
#end
|
9
|
+
end
|
10
|
+
|
11
|
+
VEGETABLES = ['Artichoke', 'Arugula', 'Asparagus', 'Amaranth', 'Bok choy', 'Broccoflower', 'Broccoli',
|
12
|
+
'Brussels sprouts', 'Cabbage', 'Calabrese', 'Cannabis', 'Carrots', 'Cauliflower', 'Celery', 'Chard',
|
13
|
+
'Collard greens', 'Corn salad', 'Eggplant', 'Endive', 'Fiddleheads', 'Frisee', 'Kale', 'Kohlrabi',
|
14
|
+
'Lettuce Lactuca sativa', 'Corn', 'Mushrooms', 'Mustard greens', 'Nettles', 'New Zealand spinach', 'Okra',
|
15
|
+
'Parsley', 'Radicchio', 'Rhubarb', 'Salsify', 'Skirret', 'Spinach', 'Topinambur', 'Tat soi', 'Tomato',
|
16
|
+
'Water chestnut', 'Watercress']
|
17
|
+
|
18
|
+
LEGUMES = ['Alfalfa sprouts', 'Azuki beans', 'Bean sprouts', 'Black beans', 'Black-eyed peas', 'Borlotti bean',
|
19
|
+
'Broad beans', 'Chickpeas', 'Green beans', 'Kidney beans', 'Lentils', 'Lima beans', 'Mung beans',
|
20
|
+
'Navy beans', 'Pinto beans', 'Runner beans', 'Soy beans', 'Snap peas']
|
21
|
+
|
22
|
+
HERBS_AND_SPICES = ['Anise', 'Basil', 'Caraway', 'Cilantro', 'Coriander', 'Chamomile', 'Dill', 'Fennel', 'Lavender',
|
23
|
+
'Lemon Grass', 'Marjoram', 'Oregano', 'Parsley', 'Rosemary', 'Sage', 'Thyme']
|
24
|
+
|
25
|
+
ONIONS = ['Chives', 'Garlic', 'Leek Allium porrum', 'Onion', 'Shallot', 'Scallion']
|
26
|
+
|
27
|
+
PEPPERS = ['Bell pepper', 'Chili pepper', 'Jalapeno', 'Habanero', 'Paprika', 'Tabasco pepper', 'Cayenne pepper']
|
28
|
+
|
29
|
+
ROOT_VEGETABLES = ['Beet', 'Carrot', 'Celeriac', 'Daikon', 'Ginger', 'Parsnip', 'Rutabaga', 'Turnip']
|
30
|
+
|
31
|
+
RADISH = ['Rutabaga', 'Turnip', 'Wasabi', 'Horseradish', 'White radish']
|
32
|
+
|
33
|
+
SQUASHES = ['Acorn squash', 'Butternut squash', 'Banana squash', 'Zucchini', 'Cucumber', 'Delicata', 'Gem squash',
|
34
|
+
'Hubbard squash', 'Squash', 'Patty pans', 'Pumpkin', 'Spaghetti squash']
|
35
|
+
|
36
|
+
TUBERS = ['Jicama', 'Jerusalem artichoke', 'Potato', 'Sweet potato', 'Taro', 'Yam']
|
@@ -0,0 +1,19 @@
|
|
1
|
+
class RankleIndex < ActiveRecord::Base
|
2
|
+
belongs_to :indexable, polymorphic: true
|
3
|
+
end
|
4
|
+
|
5
|
+
class Fruit < ActiveRecord::Base
|
6
|
+
has_many :rankle_indices, as: :indexable
|
7
|
+
end
|
8
|
+
|
9
|
+
class Vegetable < ActiveRecord::Base
|
10
|
+
has_many :rankle_indices, as: :indexable
|
11
|
+
end
|
12
|
+
|
13
|
+
class Point < ActiveRecord::Base
|
14
|
+
has_many :rankle_indices, as: :indexable
|
15
|
+
end
|
16
|
+
|
17
|
+
class Row < ActiveRecord::Base
|
18
|
+
has_many :rankle_indices, as: :indexable
|
19
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
ActiveRecord::Schema.define do
|
2
|
+
self.verbose = false
|
3
|
+
|
4
|
+
create_table :fruits, :force => true do |t|
|
5
|
+
t.string :name
|
6
|
+
|
7
|
+
t.timestamps null: false
|
8
|
+
end
|
9
|
+
|
10
|
+
create_table :vegetables, :force => true do |t|
|
11
|
+
t.string :name
|
12
|
+
|
13
|
+
t.timestamps null: false
|
14
|
+
end
|
15
|
+
|
16
|
+
create_table :points, :force => true do |t|
|
17
|
+
t.integer :x
|
18
|
+
t.integer :y
|
19
|
+
|
20
|
+
t.timestamps null: false
|
21
|
+
end
|
22
|
+
|
23
|
+
create_table :rows, :force => true do |t|
|
24
|
+
t.string :text
|
25
|
+
|
26
|
+
t.timestamps null: false
|
27
|
+
end
|
28
|
+
|
29
|
+
create_table :rankle_indices, :force => true do |t|
|
30
|
+
t.string :indexable_name
|
31
|
+
t.integer :indexable_id
|
32
|
+
t.string :indexable_type
|
33
|
+
t.integer :indexable_position
|
34
|
+
|
35
|
+
t.timestamps null: false
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'rails/generators/base'
|
2
|
+
|
3
|
+
module Rankle
|
4
|
+
module Generators
|
5
|
+
class InstallGenerator < Rails::Generators::Base
|
6
|
+
include Rails::Generators::Migration
|
7
|
+
source_root File.expand_path('../templates/', __FILE__)
|
8
|
+
|
9
|
+
def generate_migration
|
10
|
+
migration_template 'migration.rb', 'db/migrate/create_rankle_index.rb'
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.next_migration_number(dir)
|
14
|
+
Time.now.utc.strftime("%Y%m%d%H%M%S")
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
class CreateRankleIndex < ActiveRecord::Migration
|
2
|
+
def change
|
3
|
+
create_table(:rankle_index) do |t|
|
4
|
+
t.string :indexable_name
|
5
|
+
t.integer :indexable_id
|
6
|
+
t.string :indexable_type
|
7
|
+
t.integer :indexable_position
|
8
|
+
|
9
|
+
t.timestamps null: false
|
10
|
+
end
|
11
|
+
|
12
|
+
add_index :rankle_index, :indexable_name
|
13
|
+
add_index :rankle_index, :indexable_id
|
14
|
+
add_index :rankle_index, :indexable_type
|
15
|
+
add_index :rankle_index, :indexable_position
|
16
|
+
end
|
17
|
+
end
|
data/lib/rankle.rb
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'rankle/ranker'
|
3
|
+
require 'rankle/version'
|
4
|
+
|
5
|
+
# Rankle provides multi-resource ranking for ActiveRecord objects.
|
6
|
+
#
|
7
|
+
# This top-level module provides a namespace for all Rankle code.
|
8
|
+
#
|
9
|
+
# @author William Burns
|
10
|
+
module Rankle
|
11
|
+
# Class methods added to ActiveRecord models
|
12
|
+
module ClassMethods
|
13
|
+
# @return [ActiveRecord::Relation]
|
14
|
+
def ranked name = :default
|
15
|
+
ranked_results = joins("INNER JOIN rankle_indices ON rankle_indices.indexable_name = '#{name}' AND
|
16
|
+
rankle_indices.indexable_id = #{self.to_s.tableize}.id AND
|
17
|
+
rankle_indices.indexable_type = '#{self.to_s}'")
|
18
|
+
if ranked_results.size == 0
|
19
|
+
self.all
|
20
|
+
else
|
21
|
+
ranked_results.order('rankle_indices.indexable_position')
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def ranks proc
|
26
|
+
Ranker.put self, proc
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.ranked name = :default
|
31
|
+
RankleIndex.where(indexable_name: name).order(:indexable_position).map do |duck|
|
32
|
+
duck.indexable_type.classify.constantize.find(duck.indexable_id)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# instance methods added to ActiveRecord models
|
37
|
+
module InstanceMethods
|
38
|
+
def set_default_position
|
39
|
+
if Ranker.get(self.class)
|
40
|
+
position = self.class.ranked.each_with_index { |record, index| break index if Ranker.get(self.class).call(self, record) }
|
41
|
+
end unless Ranker.get(self.class).is_a?(Symbol)
|
42
|
+
position = self.class.count - 1 if position.nil? || position.is_a?(Array)
|
43
|
+
rank position
|
44
|
+
if Ranker.get(self.class).is_a?(Symbol)
|
45
|
+
rank Ranker.get(self.class), RankleIndex.where(indexable_name: Ranker.get(self.class)).count
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Assigns an explicit position to the record
|
50
|
+
#
|
51
|
+
# @param format [Integer] the new position
|
52
|
+
# @return [Integer or Exception] the new position or an exception if the position could not be set
|
53
|
+
def position= position
|
54
|
+
rank position
|
55
|
+
end
|
56
|
+
|
57
|
+
def rank name = :default, position
|
58
|
+
rankle_index = RankleIndex.where(indexable_name: name.to_s, indexable_id: id, indexable_type: self.class).first_or_create!
|
59
|
+
rankle_index_length = if name == :default
|
60
|
+
RankleIndex.where(indexable_name: name.to_s, indexable_type: self.class).count
|
61
|
+
else
|
62
|
+
RankleIndex.where(indexable_name: name.to_s).count
|
63
|
+
end
|
64
|
+
position = 0 if position < 0
|
65
|
+
position = rankle_index_length - 1 if position >= rankle_index_length
|
66
|
+
rankle_index.update_attribute(:indexable_position, rankle_index_length - 1) unless rankle_index.indexable_position
|
67
|
+
swap_distance = -1
|
68
|
+
swap_distance *= -1 if rankle_index.indexable_position < position
|
69
|
+
until rankle_index.indexable_position == position
|
70
|
+
if name == :default
|
71
|
+
Ranker.swap(rankle_index, RankleIndex.where(indexable_name: name.to_s, indexable_type: self.class, indexable_position: rankle_index.indexable_position + swap_distance).first)
|
72
|
+
else
|
73
|
+
Ranker.swap(rankle_index, RankleIndex.where(indexable_name: name.to_s, indexable_position: rankle_index.indexable_position + swap_distance).first)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def position name = :default
|
79
|
+
RankleIndex.where(indexable_name: name.to_s, indexable_id: id, indexable_type: self.class).first_or_create.indexable_position
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
ActiveRecord::Base.extend Rankle::ClassMethods
|
85
|
+
ActiveRecord::Base.send :include, Rankle::InstanceMethods
|
86
|
+
|
87
|
+
class ActiveRecord::Base
|
88
|
+
def self.inherited(child)
|
89
|
+
super
|
90
|
+
unless child == ActiveRecord::SchemaMigration || child == RankleIndex
|
91
|
+
child.send :after_create, :set_default_position
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Rankle
|
2
|
+
class Ranker
|
3
|
+
def self.put klass, proc
|
4
|
+
@rankers ||= {}
|
5
|
+
@rankers[klass] = proc
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.get klass
|
9
|
+
@rankers[klass] rescue nil
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.swap(first_index, second_index)
|
13
|
+
first_index_position = first_index.indexable_position
|
14
|
+
first_index.update_attribute(:indexable_position, second_index.indexable_position)
|
15
|
+
second_index.update_attribute(:indexable_position, first_index_position)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/rankle.gemspec
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'rankle/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "rankle"
|
8
|
+
spec.version = Rankle::VERSION
|
9
|
+
spec.authors = ["Wil"]
|
10
|
+
spec.email = ["rankle@william-burns.com"]
|
11
|
+
spec.summary = %q{Rankle provides multi-resource ranking.}
|
12
|
+
spec.description = %q{Rankle provides multi-resource ranking.}
|
13
|
+
spec.homepage = ""
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency "activerecord"
|
22
|
+
spec.add_development_dependency "bundler", "~> 1.7"
|
23
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
24
|
+
spec.add_development_dependency "cucumber"
|
25
|
+
spec.add_development_dependency "sqlite3"
|
26
|
+
spec.add_development_dependency "rspec-expectations"
|
27
|
+
spec.add_development_dependency "database_cleaner"
|
28
|
+
spec.add_development_dependency "factory_girl_rails"
|
29
|
+
spec.add_development_dependency "yard"
|
30
|
+
end
|
metadata
ADDED
@@ -0,0 +1,211 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rankle
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.0.pre
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Wil
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-05-26 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: '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
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.7'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.7'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '10.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '10.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: cucumber
|
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: sqlite3
|
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: rspec-expectations
|
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: database_cleaner
|
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: factory_girl_rails
|
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: yard
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
description: Rankle provides multi-resource ranking.
|
140
|
+
email:
|
141
|
+
- rankle@william-burns.com
|
142
|
+
executables: []
|
143
|
+
extensions: []
|
144
|
+
extra_rdoc_files: []
|
145
|
+
files:
|
146
|
+
- ".gitignore"
|
147
|
+
- Gemfile
|
148
|
+
- LICENSE
|
149
|
+
- LICENSE.txt
|
150
|
+
- README.md
|
151
|
+
- Rakefile
|
152
|
+
- features/default_ranking.feature
|
153
|
+
- features/get_position.feature
|
154
|
+
- features/multi-resource_ranking.feature
|
155
|
+
- features/named_ranking.feature
|
156
|
+
- features/ranking.feature
|
157
|
+
- features/set_position.feature
|
158
|
+
- features/step_definitions/fruit_steps.rb
|
159
|
+
- features/step_definitions/model_steps.rb
|
160
|
+
- features/step_definitions/position_steps.rb
|
161
|
+
- features/support/env.rb
|
162
|
+
- features/support/factories/fruit.rb
|
163
|
+
- features/support/factories/vegetable.rb
|
164
|
+
- features/support/models.rb
|
165
|
+
- features/support/schema.rb
|
166
|
+
- lib/generators/rankle/install_generator.rb
|
167
|
+
- lib/generators/rankle/templates/migration.rb
|
168
|
+
- lib/rankle.rb
|
169
|
+
- lib/rankle/ranker.rb
|
170
|
+
- lib/rankle/version.rb
|
171
|
+
- rankle.gemspec
|
172
|
+
homepage: ''
|
173
|
+
licenses:
|
174
|
+
- MIT
|
175
|
+
metadata: {}
|
176
|
+
post_install_message:
|
177
|
+
rdoc_options: []
|
178
|
+
require_paths:
|
179
|
+
- lib
|
180
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
181
|
+
requirements:
|
182
|
+
- - ">="
|
183
|
+
- !ruby/object:Gem::Version
|
184
|
+
version: '0'
|
185
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
186
|
+
requirements:
|
187
|
+
- - ">"
|
188
|
+
- !ruby/object:Gem::Version
|
189
|
+
version: 1.3.1
|
190
|
+
requirements: []
|
191
|
+
rubyforge_project:
|
192
|
+
rubygems_version: 2.2.2
|
193
|
+
signing_key:
|
194
|
+
specification_version: 4
|
195
|
+
summary: Rankle provides multi-resource ranking.
|
196
|
+
test_files:
|
197
|
+
- features/default_ranking.feature
|
198
|
+
- features/get_position.feature
|
199
|
+
- features/multi-resource_ranking.feature
|
200
|
+
- features/named_ranking.feature
|
201
|
+
- features/ranking.feature
|
202
|
+
- features/set_position.feature
|
203
|
+
- features/step_definitions/fruit_steps.rb
|
204
|
+
- features/step_definitions/model_steps.rb
|
205
|
+
- features/step_definitions/position_steps.rb
|
206
|
+
- features/support/env.rb
|
207
|
+
- features/support/factories/fruit.rb
|
208
|
+
- features/support/factories/vegetable.rb
|
209
|
+
- features/support/models.rb
|
210
|
+
- features/support/schema.rb
|
211
|
+
has_rdoc:
|