resort-bjones 0.3.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.
- data/.rspec +2 -0
- data/.rvmrc +1 -0
- data/.travis.yml +8 -0
- data/Gemfile +20 -0
- data/LICENSE +20 -0
- data/Rakefile +46 -0
- data/Readme.md +177 -0
- data/VERSION +1 -0
- data/lib/generators/active_record/resort_generator.rb +30 -0
- data/lib/generators/active_record/templates/migration.rb +17 -0
- data/lib/resort-bjones.rb +1 -0
- data/lib/resort.rb +237 -0
- data/lib/resort/version.rb +4 -0
- data/resort-bjones.gemspec +76 -0
- data/spec/generators/migration_spec.rb +38 -0
- data/spec/resort_spec.rb +447 -0
- data/spec/spec_helper.rb +64 -0
- metadata +160 -0
data/.rspec
ADDED
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm --create use ruby-1.9.3@resort
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
source "http://rubygems.org"
|
2
|
+
|
3
|
+
gem 'activerecord', '>= 3.0.5', '< 3.2'
|
4
|
+
|
5
|
+
group :development, :test do
|
6
|
+
gem 'jeweler', '>= 1.5.2'
|
7
|
+
gem 'rake'
|
8
|
+
gem 'sqlite3'
|
9
|
+
gem 'rspec'
|
10
|
+
gem 'yard'
|
11
|
+
gem 'bluecloth'
|
12
|
+
gem 'generator_spec', '~> 0.8.1'
|
13
|
+
|
14
|
+
# For debugging under ruby 1.9 special gems are needed
|
15
|
+
# gem 'ruby-debug19', :platform => :mri
|
16
|
+
# See http://blog.wyeworks.com/2011/11/1/ruby-1-9-3-and-ruby-debug
|
17
|
+
# gem 'ruby-debug-base19', '>=0.11.26'
|
18
|
+
# gem 'linecache19', '>=0.5.13'
|
19
|
+
end
|
20
|
+
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2011 Codegram
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler'
|
3
|
+
begin
|
4
|
+
Bundler.setup(:default, :development)
|
5
|
+
rescue Bundler::BundlerError => e
|
6
|
+
$stderr.puts e.message
|
7
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
8
|
+
exit e.status_code
|
9
|
+
end
|
10
|
+
require 'rake'
|
11
|
+
|
12
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
13
|
+
|
14
|
+
require 'jeweler'
|
15
|
+
Jeweler::Tasks.new do |gem|
|
16
|
+
gem.name = "resort-bjones"
|
17
|
+
gem.version = version
|
18
|
+
gem.homepage = "http://github.com/bjones/resort"
|
19
|
+
gem.license = "MIT"
|
20
|
+
gem.summary = %Q{Positionless model sorting for Rails 3.}
|
21
|
+
gem.description = %Q{Positionless model sorting for Rails 3.}
|
22
|
+
gem.email = "cbj@gnu.org"
|
23
|
+
gem.authors = ["Oriol Gual", "Josep M. Bach", "Josep Jaume Rey", "Brian Jones"]
|
24
|
+
end
|
25
|
+
Jeweler::RubygemsDotOrgTasks.new
|
26
|
+
|
27
|
+
require 'rspec/core'
|
28
|
+
require 'rspec/core/rake_task'
|
29
|
+
RSpec::Core::RakeTask.new(:spec) do |spec|
|
30
|
+
end
|
31
|
+
|
32
|
+
task :default => :spec
|
33
|
+
|
34
|
+
require 'yard'
|
35
|
+
YARD::Rake::YardocTask.new(:docs) do |t|
|
36
|
+
t.files = ['lib/**/*.rb']
|
37
|
+
t.options = ['-m', 'markdown', '--no-private', '-r', 'Readme.md', '--title', 'Resort documentation']
|
38
|
+
end
|
39
|
+
|
40
|
+
task :doc => :docs
|
41
|
+
|
42
|
+
desc "Generate and open class diagram (needs Graphviz installed)"
|
43
|
+
task :graph do |t|
|
44
|
+
`bundle exec yard graph -d --full --no-private | dot -Tpng -o graph.png && open graph.png`
|
45
|
+
end
|
46
|
+
|
data/Readme.md
ADDED
@@ -0,0 +1,177 @@
|
|
1
|
+
#resort
|
2
|
+
|
3
|
+
Resort provides sorting capabilities to your Rails 3 models.
|
4
|
+
|
5
|
+
##Versions
|
6
|
+
|
7
|
+
This is a fork of resort, named resort-bjones, which adds ActiveRecord 3.1 support.
|
8
|
+
|
9
|
+
* resort-bjones 0.3.0 should work with ActiveRecord 3.0 and ActiveRecord 3.1
|
10
|
+
* resort 0.2.3 works with ActiveRecord 3.0 only
|
11
|
+
|
12
|
+
##Install
|
13
|
+
|
14
|
+
$ gem install resort-bjones
|
15
|
+
|
16
|
+
Or in your Gemfile:
|
17
|
+
|
18
|
+
```ruby
|
19
|
+
gem 'resort-bjones'
|
20
|
+
```
|
21
|
+
|
22
|
+
##Rationale
|
23
|
+
|
24
|
+
Most other sorting plugins work with an absolute `position` attribute that sets
|
25
|
+
the _weight_ of a given element within a tree. This field has no semantic sense,
|
26
|
+
since "84" by itself gives you absolutely no information about an element's
|
27
|
+
position or its relations with other elements of the tree.
|
28
|
+
|
29
|
+
Resort is implemented like a [linked list](http://en.wikipedia.org/wiki/Linked_list),
|
30
|
+
rather than relying on absolute position values. This way, every model
|
31
|
+
references a `next` element, which seems a bit more sensible :)
|
32
|
+
|
33
|
+
##Usage
|
34
|
+
|
35
|
+
First, run the migration for the model you want to Resort:
|
36
|
+
|
37
|
+
$ rails generate resort:migration product
|
38
|
+
$ rake db:migrate
|
39
|
+
|
40
|
+
Then in your Product model:
|
41
|
+
|
42
|
+
```ruby
|
43
|
+
class Product < ActiveRecord::Base
|
44
|
+
resort!
|
45
|
+
end
|
46
|
+
```
|
47
|
+
|
48
|
+
**NOTE**: By default, Resort will treat _all products_ as a single big tree.
|
49
|
+
If you wanted to limit the tree scope, i.e. treating every ProductLine as a
|
50
|
+
separate tree of sortable products, you must override the `siblings` method:
|
51
|
+
|
52
|
+
```ruby
|
53
|
+
class Product < ActiveRecord::Base
|
54
|
+
resort!
|
55
|
+
|
56
|
+
def siblings
|
57
|
+
# Tree contains only products from my own product line
|
58
|
+
self.product_line.products
|
59
|
+
end
|
60
|
+
end
|
61
|
+
```
|
62
|
+
|
63
|
+
### Concurrency
|
64
|
+
|
65
|
+
Multiple users modifying the same list at the same time could be a problem,
|
66
|
+
so it's always a good practice to wrap the changes in a transaction:
|
67
|
+
|
68
|
+
```ruby
|
69
|
+
Product.transaction do
|
70
|
+
my_product.append_to(another_product)
|
71
|
+
end
|
72
|
+
```
|
73
|
+
|
74
|
+
###API
|
75
|
+
|
76
|
+
**Every time a product is created, it will be appended after the last element.**
|
77
|
+
|
78
|
+
Moreover, now a `product` responds to the following methods:
|
79
|
+
|
80
|
+
* `first?` — Returns true if the element is the first of the tree.
|
81
|
+
* `append_to(other_element)` — Appends the element _after_ another element.
|
82
|
+
* `next` — Returns the next element in the list
|
83
|
+
* `previous` — Returns the previous element in the list
|
84
|
+
|
85
|
+
And the class Product has a new scope named `ordered` that returns the
|
86
|
+
products in order.
|
87
|
+
|
88
|
+
### Examples
|
89
|
+
|
90
|
+
Given our `Product` example defined before, we can do things like:
|
91
|
+
|
92
|
+
Getting products in order:
|
93
|
+
|
94
|
+
```ruby
|
95
|
+
Product.first_in_order # returns the first ordered product.
|
96
|
+
Product.last_in_order # returns the last ordered product.
|
97
|
+
Product.ordered # returns all products ordered as an Array, not a Relation!
|
98
|
+
```
|
99
|
+
|
100
|
+
Find ordered products with scopes or conditions:
|
101
|
+
|
102
|
+
```ruby
|
103
|
+
Product.where('price > 10').ordered # => Ordered array of products with price > 10
|
104
|
+
Product.with_custom_scope.ordered # => Ordered array of products with your custom conditions
|
105
|
+
```
|
106
|
+
|
107
|
+
Modify the list of products:
|
108
|
+
|
109
|
+
```ruby
|
110
|
+
product = Product.create(:name => 'Bread')
|
111
|
+
product.first? # => true
|
112
|
+
|
113
|
+
another_product = Product.create(:name => 'Milk')
|
114
|
+
yet_another_product = Product.create(:name => 'Salami')
|
115
|
+
|
116
|
+
yet_another_product.append_to(product) # puts the products right after the first one
|
117
|
+
|
118
|
+
Product.ordered.map(&:name) # => ['Bread', 'Salami', 'Milk']
|
119
|
+
```
|
120
|
+
|
121
|
+
Check neighbours:
|
122
|
+
|
123
|
+
```ruby
|
124
|
+
product = Product.create(:name => 'Bread')
|
125
|
+
second_product = Product.create(:name => 'Milk')
|
126
|
+
third_product = Product.create(:name => 'Salami')
|
127
|
+
|
128
|
+
second_product.previous.name # => 'Bread'
|
129
|
+
second_product.next.name # => 'Salami'
|
130
|
+
|
131
|
+
third_product.next # => nil
|
132
|
+
```
|
133
|
+
|
134
|
+
Maybe you need different orders depending on the product vendor:
|
135
|
+
|
136
|
+
```ruby
|
137
|
+
class Product < ActiveRecord::Base
|
138
|
+
resort!
|
139
|
+
|
140
|
+
belongs_to :vendor
|
141
|
+
|
142
|
+
def siblings
|
143
|
+
self.vendor.products
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
bread = Product.create(:name => 'Bread', :vendor => Vendor.where(:name => 'Bread factory'))
|
148
|
+
bread.first? # => true
|
149
|
+
|
150
|
+
milk = Product.create(:name => 'Milk', :vendor => Vendor.where(:name => 'Cow world'))
|
151
|
+
milk.first? # => true
|
152
|
+
|
153
|
+
# bread and milk aren't neighbours
|
154
|
+
```
|
155
|
+
|
156
|
+
##Under the hood
|
157
|
+
|
158
|
+
Run the test suite by typing:
|
159
|
+
|
160
|
+
rake spec
|
161
|
+
|
162
|
+
You can also build the documentation with the following command:
|
163
|
+
|
164
|
+
rake docs
|
165
|
+
|
166
|
+
## Note on Patches/Pull Requests
|
167
|
+
|
168
|
+
* Fork the project.
|
169
|
+
* Make your feature addition or bug fix.
|
170
|
+
* Add tests for it. This is important so I don't break it in a
|
171
|
+
future version unintentionally.
|
172
|
+
* Commit, do not mess with rakefile, version, or history. (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
|
173
|
+
* Send us a pull request. Bonus points for topic branches.
|
174
|
+
|
175
|
+
## Copyright
|
176
|
+
|
177
|
+
Copyright (c) 2011 Codegram. See LICENSE for details.
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.3.0
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
require 'rails/generators/active_record'
|
3
|
+
|
4
|
+
module Resort
|
5
|
+
# Module containing Resort generators
|
6
|
+
module Generators
|
7
|
+
|
8
|
+
# Rails generator to add a migration for Resort
|
9
|
+
class MigrationGenerator < ActiveRecord::Generators::Base
|
10
|
+
# Implement the required interface for `Rails::Generators::Migration`.
|
11
|
+
# Taken from `ActiveRecord` code.
|
12
|
+
# @see http://github.com/rails/rails/blob/master/activerecord/lib/generators/active_record.rb
|
13
|
+
def self.next_migration_number(dirname)
|
14
|
+
if ActiveRecord::Base.timestamped_migrations
|
15
|
+
Time.now.utc.strftime("%Y%m%d%H%M%S")
|
16
|
+
else
|
17
|
+
"%.3d" % (current_migration_number(dirname) + 1)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
desc "Creates a Resort migration."
|
22
|
+
source_root File.expand_path("../templates", __FILE__)
|
23
|
+
|
24
|
+
# Copies a migration file adding resort fields to a given model
|
25
|
+
def copy_migration_file
|
26
|
+
migration_template 'migration.rb', "db/migrate/add_resort_fields_to_#{table_name.pluralize}.rb"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# Migration to add the necessary fields to a resorted model
|
2
|
+
class AddResortFieldsTo<%= table_name.camelize %> < ActiveRecord::Migration
|
3
|
+
# Adds Resort fields, next_id and first, and indexes to a given model
|
4
|
+
def self.up
|
5
|
+
add_column :<%= table_name %>, :next_id, :integer
|
6
|
+
add_column :<%= table_name %>, :first, :boolean
|
7
|
+
add_index :<%= table_name %>, :next_id
|
8
|
+
add_index :<%= table_name %>, :first
|
9
|
+
end
|
10
|
+
|
11
|
+
# Removes Resort fields
|
12
|
+
def self.down
|
13
|
+
remove_column :<%= table_name %>, :next_id
|
14
|
+
remove_column :<%= table_name %>, :first
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
@@ -0,0 +1 @@
|
|
1
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'resort'))
|
data/lib/resort.rb
ADDED
@@ -0,0 +1,237 @@
|
|
1
|
+
require 'generators/active_record/resort_generator' if defined?(Rails)
|
2
|
+
require 'active_record' unless defined?(ActiveRecord)
|
3
|
+
|
4
|
+
# # Resort
|
5
|
+
#
|
6
|
+
# A tool that allows any ActiveRecord model to be sorted.
|
7
|
+
#
|
8
|
+
# Unlike most Rails sorting plugins (acts_as_list, etc), Resort is based
|
9
|
+
# on linked lists rather than absolute position fields.
|
10
|
+
#
|
11
|
+
# @example Using Resort in an ActiveRecord model
|
12
|
+
# # In the migration
|
13
|
+
# create_table :products do |t|
|
14
|
+
# t.text :name
|
15
|
+
# t.references :next
|
16
|
+
# t.boolean :first
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# # Model
|
20
|
+
# class Product < ActiveRecord::Base
|
21
|
+
# resort!
|
22
|
+
#
|
23
|
+
# # A sortable model must implement #siblings method, which should
|
24
|
+
# # return and ActiveRecord::Relation with all the models to be
|
25
|
+
# # considered as `peers` in the list representing the sorted
|
26
|
+
# # products, i.e. its siblings.
|
27
|
+
# def siblings
|
28
|
+
# self.class.scoped
|
29
|
+
# end
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# product = Product.create(:name => 'Bread')
|
33
|
+
# product.first? # => true
|
34
|
+
#
|
35
|
+
# another_product = Product.create(:name => 'Milk')
|
36
|
+
# yet_another_product = Product.create(:name => 'Salami')
|
37
|
+
#
|
38
|
+
# yet_another_product.append_to(product)
|
39
|
+
#
|
40
|
+
# Product.ordered.map(&:name)
|
41
|
+
# # => ['Bread', 'Salami', 'Milk']
|
42
|
+
module Resort
|
43
|
+
# The module encapsulating all the Resort functionality.
|
44
|
+
#
|
45
|
+
# @todo Refactor into a more OO solution, maybe implementing a LinkedList
|
46
|
+
# object.
|
47
|
+
module Sortable
|
48
|
+
class << self
|
49
|
+
# When included, extends the includer with {ClassMethods}, and includes
|
50
|
+
# {InstanceMethods} in it.
|
51
|
+
#
|
52
|
+
# It also establishes the required relationships. It is necessary that
|
53
|
+
# the includer table has the following database columns:
|
54
|
+
#
|
55
|
+
# t.references :next
|
56
|
+
# t.boolean :first
|
57
|
+
#
|
58
|
+
# @param [ActiveRecord::Base] base the includer `ActiveRecord` model.
|
59
|
+
def included(base)
|
60
|
+
base.extend ClassMethods
|
61
|
+
base.send :include, InstanceMethods
|
62
|
+
|
63
|
+
base.has_one :previous, :class_name => base.name, :foreign_key => 'next_id', :inverse_of => :next
|
64
|
+
base.belongs_to :next, :class_name => base.name, :inverse_of => :previous
|
65
|
+
|
66
|
+
base.after_create :include_in_list!
|
67
|
+
base.after_destroy :delete_from_list
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Class methods to be used from the model class.
|
72
|
+
module ClassMethods
|
73
|
+
# Returns the first element of the list.
|
74
|
+
#
|
75
|
+
# @return [ActiveRecord::Base] the first element of the list.
|
76
|
+
def first_in_order
|
77
|
+
scoped.where(:first => true).first
|
78
|
+
end
|
79
|
+
|
80
|
+
# Returns the last element of the list.
|
81
|
+
#
|
82
|
+
# @return [ActiveRecord::Base] the last element of the list.
|
83
|
+
def last_in_order
|
84
|
+
scoped.where(:next_id => nil).first
|
85
|
+
end
|
86
|
+
|
87
|
+
|
88
|
+
# Returns eager-loaded Components in order.
|
89
|
+
#
|
90
|
+
# OPTIMIZE: Use IdentityMap when available
|
91
|
+
# @return [Array<ActiveRecord::Base>] the ordered elements
|
92
|
+
def ordered
|
93
|
+
ordered_elements = []
|
94
|
+
elements = {}
|
95
|
+
|
96
|
+
scoped.each do |element|
|
97
|
+
if ordered_elements.empty? && element.first?
|
98
|
+
ordered_elements << element
|
99
|
+
else
|
100
|
+
elements[element.id] = element
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
raise "Multiple or no first items in the list where found. Consider defining a siblings method" if ordered_elements.length != 1 && elements.length > 0
|
105
|
+
|
106
|
+
elements.length.times do
|
107
|
+
ordered_elements << elements[ordered_elements.last.next_id]
|
108
|
+
end
|
109
|
+
ordered_elements.compact
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# Instance methods to use.
|
114
|
+
module InstanceMethods
|
115
|
+
|
116
|
+
# Default definition of siblings, i.e. every instance of the model.
|
117
|
+
#
|
118
|
+
# Can be overriden to specify a different scope for the siblings.
|
119
|
+
# For example, if we wanted to limit a products tree inside a ProductLine
|
120
|
+
# scope, we would do the following:
|
121
|
+
#
|
122
|
+
# class Product < ActiveRecord::Base
|
123
|
+
# belongs_to :product_line
|
124
|
+
#
|
125
|
+
# resort!
|
126
|
+
#
|
127
|
+
# def siblings
|
128
|
+
# self.product_line.products
|
129
|
+
# end
|
130
|
+
#
|
131
|
+
# This way, every product line is an independent tree of sortable
|
132
|
+
# products.
|
133
|
+
#
|
134
|
+
# @return [ActiveRecord::Relation] the element's siblings relation.
|
135
|
+
def siblings
|
136
|
+
self.class.scoped
|
137
|
+
end
|
138
|
+
# Includes the object in the linked list.
|
139
|
+
#
|
140
|
+
# If there are no other objects, it prepends the object so that it is
|
141
|
+
# in the first position. Otherwise, it appends it to the end of the
|
142
|
+
# empty list.
|
143
|
+
def include_in_list!
|
144
|
+
self.class.transaction do
|
145
|
+
self.lock!
|
146
|
+
_siblings.count > 0 ? last!\
|
147
|
+
: prepend
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
# Puts the object in the first position of the list.
|
152
|
+
def prepend
|
153
|
+
self.class.transaction do
|
154
|
+
self.lock!
|
155
|
+
return if first?
|
156
|
+
if _siblings.count > 0
|
157
|
+
delete_from_list
|
158
|
+
old_first = _siblings.first_in_order
|
159
|
+
raise(ActiveRecord::RecordNotSaved) unless self.update_attribute(:next_id, old_first.id)
|
160
|
+
raise(ActiveRecord::RecordNotSaved) unless old_first.update_attribute(:first, false)
|
161
|
+
end
|
162
|
+
raise(ActiveRecord::RecordNotSaved) unless self.update_attribute(:first, true)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
# Puts the object in the last position of the list.
|
167
|
+
def push
|
168
|
+
self.class.transaction do
|
169
|
+
self.lock!
|
170
|
+
self.append_to(_siblings.last_in_order) unless last?
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
# Puts the object right after another object in the list.
|
175
|
+
def append_to(another)
|
176
|
+
self.class.transaction do
|
177
|
+
self.lock!
|
178
|
+
return if another.next_id == id
|
179
|
+
another.lock!
|
180
|
+
delete_from_list
|
181
|
+
if self.next_id or (another && another.next_id)
|
182
|
+
raise(ActiveRecord::RecordNotSaved) unless self.update_attribute(:next_id, another.next_id)
|
183
|
+
end
|
184
|
+
if another
|
185
|
+
raise(ActiveRecord::RecordNotSaved) unless another.update_attribute(:next_id, self.id)
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
private
|
191
|
+
|
192
|
+
def delete_from_list
|
193
|
+
if first? && self.next
|
194
|
+
self.next.lock!
|
195
|
+
raise(ActiveRecord::RecordNotSaved) unless self.next.update_attribute(:first, true)
|
196
|
+
elsif self.previous
|
197
|
+
self.previous.lock!
|
198
|
+
p = self.previous
|
199
|
+
self.previous = nil unless frozen?
|
200
|
+
raise(ActiveRecord::RecordNotSaved) unless p.update_attribute(:next_id, self.next_id)
|
201
|
+
end
|
202
|
+
unless frozen?
|
203
|
+
self.first = false
|
204
|
+
self.next = nil
|
205
|
+
save!
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
def last?
|
210
|
+
!self.first && !self.next_id
|
211
|
+
end
|
212
|
+
|
213
|
+
def last!
|
214
|
+
self.class.transaction do
|
215
|
+
self.lock!
|
216
|
+
raise(ActiveRecord::RecordNotSaved) unless _siblings.last_in_order.update_attribute(:next_id, self.id)
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
def _siblings
|
221
|
+
table = self.class.arel_table
|
222
|
+
siblings.where(table[:id].not_eq(self.id))
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
# Helper class methods to be injected into ActiveRecord::Base class.
|
227
|
+
# They will be available to every model.
|
228
|
+
module ClassMethods
|
229
|
+
# Helper class method to include Resort::Sortable in an ActiveRecord
|
230
|
+
# model.
|
231
|
+
def resort!
|
232
|
+
include Sortable
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
ActiveRecord::Base.extend Resort::ClassMethods
|