resort-bjones 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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