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 ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format documentation
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm --create use ruby-1.9.3@resort
@@ -0,0 +1,8 @@
1
+ # http://about.travis-ci.org/docs/user/build-configuration/
2
+ rvm:
3
+ - 1.8.7
4
+ - 1.9.2
5
+ - ree
6
+ - ruby-head
7
+ - rbx
8
+ - rbx-2.0
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.
@@ -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
+
@@ -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?` &mdash; Returns true if the element is the first of the tree.
81
+ * `append_to(other_element)` &mdash; Appends the element _after_ another element.
82
+ * `next` &mdash; Returns the next element in the list
83
+ * `previous` &mdash; 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'))
@@ -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