resort 0.0.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/.gitignore CHANGED
@@ -5,3 +5,4 @@ pkg/*
5
5
  graph.png
6
6
  .yardoc/*
7
7
  doc/*
8
+ tmp
data/Readme.md CHANGED
@@ -23,21 +23,10 @@ references a `next` element, which seems a bit more sensible :)
23
23
 
24
24
  ##Usage
25
25
 
26
- You must add two fields (`next_id` and `first`) to your model's table:
27
-
28
- class AddResortFieldsToProducts < ActiveRecord::Migration
29
- def self.up
30
- add_column :products, :next_id, :integer
31
- add_column :products, :first, :boolean
32
- add_index :products, :next_id
33
- add_index :products, :first
34
- end
26
+ First, run the migration for the model you want to Resort:
35
27
 
36
- def self.down
37
- remove_column :products, :next_id
38
- remove_column :products, :first
39
- end
40
- end
28
+ $ rails generate resort:migration product
29
+ $ rake db:migrate
41
30
 
42
31
  Then in your Product model:
43
32
 
@@ -57,6 +46,15 @@ separate tree of sortable products, you must override the `siblings` method:
57
46
  self.product_line.products
58
47
  end
59
48
  end
49
+
50
+ ### Concurrency
51
+
52
+ Multiple users modifying the same list at the same time could be a problem,
53
+ so it's always a good practice to wrap the changes in a transaction:
54
+
55
+ Product.transaction do
56
+ my_product.append_to(another_product)
57
+ end
60
58
 
61
59
  ###API
62
60
 
@@ -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
+
@@ -1,4 +1,4 @@
1
1
  module Resort
2
2
  # Resort's version number
3
- VERSION = "0.0.2"
3
+ VERSION = "0.2.0"
4
4
  end
data/lib/resort.rb CHANGED
@@ -1,3 +1,5 @@
1
+ require 'generators/active_record/resort_generator' if defined?(Rails)
2
+
1
3
  # # Resort
2
4
  #
3
5
  # A tool that allows any ActiveRecord model to be sorted.
@@ -71,12 +73,20 @@ module Resort
71
73
  #
72
74
  # @return [ActiveRecord::Base] the first element of the list.
73
75
  def first_in_order
74
- where(:first => true).first
76
+ scoped.where(:first => true).first
77
+ end
78
+
79
+ # Returns the last element of the list.
80
+ #
81
+ # @return [ActiveRecord::Base] the last element of the list.
82
+ def last_in_order
83
+ scoped.where(:next_id => nil).first
75
84
  end
85
+
76
86
 
77
87
  # Returns eager-loaded Components in order.
78
88
  #
79
- # OPTIMIZE: Avoid creating as many hashes.
89
+ # OPTIMIZE: Use IdentityMap when available
80
90
  # @return [Array<ActiveRecord::Base>] the ordered elements
81
91
  def ordered
82
92
  ordered_elements = []
@@ -90,10 +100,12 @@ module Resort
90
100
  end
91
101
  end
92
102
 
103
+ raise "Multiple or no first items in the list where found. Consider defining a siblings method" if ordered_elements.length != 1 && elements.length > 0
104
+
93
105
  elements.length.times do
94
106
  ordered_elements << elements[ordered_elements.last.next_id]
95
107
  end
96
- ordered_elements
108
+ ordered_elements.compact
97
109
  end
98
110
  end
99
111
 
@@ -146,47 +158,46 @@ module Resort
146
158
 
147
159
  # Puts the object in the last position of the list.
148
160
  def push
149
- self.append_to(last) unless last?
161
+ self.append_to(_siblings.last_in_order) unless last?
150
162
  end
151
163
 
152
164
  # Puts the object right after another object in the list.
153
165
  def append_to(another)
154
- if self.next
155
- delete_from_list
156
- elsif last? && self.previous
157
- # self.previous.update_attribute(:next_id, nil)
158
- self.previous = nil
159
- end
166
+ return if another.next_id == id
160
167
 
161
- self.update_attribute(:next_id, another.next_id) if self.next_id or (another && another.next_id)
162
- another.update_attribute(:next_id, self.id) if another
168
+ delete_from_list
169
+
170
+ self.class.transaction do
171
+ self.update_attribute(:next_id, another.next_id) if self.next_id or (another && another.next_id)
172
+ another.update_attribute(:next_id, self.id) if another
173
+ end
163
174
  end
164
175
 
165
176
  private
166
177
 
167
178
  def delete_from_list
168
- if first? && self.next
169
- self.update_attribute(:first, nil) unless frozen?
170
- self.next.first = true
171
- self.next.previous = nil
172
- self.next.save!
173
- elsif self.previous
174
- previous.next = self.next
175
- previous.save!
176
- self.update_attribute(:next_id, nil) unless frozen?
179
+ self.class.transaction do
180
+ if first? && self.next
181
+ self.next.update_attribute(:first, true)
182
+ elsif self.previous
183
+ self.previous.update_attribute(:next_id, self.next_id)
184
+ end
185
+
186
+ unless frozen?
187
+ self.first = false
188
+ self.next = nil
189
+ self.previous = nil
190
+ save!
191
+ end
177
192
  end
178
193
  end
179
194
 
180
195
  def last?
181
- self.first != true && self.next_id == nil
182
- end
183
-
184
- def last
185
- _siblings.where(:next_id => nil).first
196
+ !self.first && !self.next_id
186
197
  end
187
198
 
188
199
  def last!
189
- last.update_attribute(:next_id, self.id)
200
+ _siblings.last_in_order.update_attribute(:next_id, self.id)
190
201
  end
191
202
 
192
203
  def _siblings
data/resort.gemspec CHANGED
@@ -19,6 +19,7 @@ Gem::Specification.new do |s|
19
19
  s.add_development_dependency 'rspec', '~> 2.5.0'
20
20
  s.add_development_dependency 'yard'
21
21
  s.add_development_dependency 'bluecloth'
22
+ s.add_development_dependency 'generator_spec', '~> 0.8.1'
22
23
 
23
24
  s.files = `git ls-files`.split("\n")
24
25
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
@@ -0,0 +1,38 @@
1
+ require 'spec_helper'
2
+ require 'action_controller'
3
+ require 'action_view'
4
+ ActionView::Template::Handlers::ERB::ENCODING_FLAG = ActionView::ENCODING_FLAG
5
+ require 'generator_spec/test_case'
6
+
7
+ module Resort
8
+ module Generators
9
+ describe MigrationGenerator do
10
+ include GeneratorSpec::TestCase
11
+ destination File.expand_path('../../../tmp', __FILE__)
12
+ tests MigrationGenerator
13
+ arguments %w(article)
14
+
15
+ before(:all) do
16
+ prepare_destination
17
+ mkdir File.join(self.test_case.destination_root, 'config')
18
+ run_generator
19
+ end
20
+
21
+ it 'generates Resort migration' do
22
+ destination_root.should have_structure {
23
+
24
+ directory "db" do
25
+ directory "migrate" do
26
+ migration "add_resort_fields_to_articles" do
27
+ contains "class AddResortFieldsToArticles"
28
+ contains ":articles, :next_id"
29
+ contains ":articles, :first"
30
+ end
31
+ end
32
+ end
33
+
34
+ }
35
+ end
36
+ end
37
+ end
38
+ end
data/spec/resort_spec.rb CHANGED
@@ -1,9 +1,5 @@
1
1
  require 'spec_helper'
2
2
 
3
- class Article < ActiveRecord::Base
4
- resort!
5
- end
6
-
7
3
  module Resort
8
4
  describe Sortable do
9
5
 
@@ -26,15 +22,8 @@ module Resort
26
22
  end
27
23
 
28
24
  describe 'ClassMethods' do
29
- describe "#first_in_order" do
30
- it 'returns the first element of the list' do
31
- first = double :article
32
- Article.should_receive(:where).with(:first => true).and_return [first]
33
25
 
34
- Article.first_in_order
35
- end
36
- end
37
- describe "#ordered" do
26
+ describe "ordering" do
38
27
  before do
39
28
  Article.destroy_all
40
29
 
@@ -42,20 +31,85 @@ module Resort
42
31
  Article.create(:name => i.to_s)
43
32
  end
44
33
 
45
- @article1 = Article.find_by_name('0')
46
- @article2 = Article.find_by_name('1')
47
- @article3 = Article.find_by_name('2')
48
- @article4 = Article.find_by_name('3')
34
+ Article.find_by_name('0').append_to(Article.find_by_name('3'))
35
+ Article.find_by_name('1').append_to(Article.find_by_name('3'))
36
+ Article.find_by_name('2').append_to(Article.find_by_name('3'))
37
+
38
+ @article1 = Article.find_by_name('3')
39
+ @article2 = Article.find_by_name('2')
40
+ @article3 = Article.find_by_name('1')
41
+ @article4 = Article.find_by_name('0')
49
42
  end
50
- it 'returns the first element of the list' do
51
- Article.ordered.should == [@article1, @article2, @article3, @article4]
43
+
44
+ describe "#first_in_order" do
45
+ it 'returns the first element of the list' do
46
+ Article.first_in_order.should == @article1
47
+ end
52
48
  end
49
+
50
+ describe "#last_in_order" do
51
+ it 'returns the last element of the list' do
52
+ Article.last_in_order.should == @article4
53
+ end
54
+ end
55
+
56
+ describe "#ordered" do
57
+ it 'returns all elements ordered' do
58
+ Article.ordered.should == [@article1, @article2, @article3, @article4]
59
+ end
60
+ end
61
+
53
62
  after do
54
63
  Article.destroy_all
55
64
  end
56
65
  end
57
66
  end
58
67
 
68
+ describe "siblings" do
69
+ before do
70
+ one_list = List.create(:name => 'My list')
71
+ another_list = List.create(:name => 'My other list')
72
+
73
+ 4.times do |i|
74
+ one_list.items << ListItem.new(:name => "My list item #{i}")
75
+ another_list.items << ListItem.new(:name => "My other list item #{i}")
76
+ end
77
+
78
+ end
79
+
80
+ describe "#first_in_order" do
81
+ it 'returns the first element of the list' do
82
+ List.find_by_name('My list').items.first_in_order.name.should == "My list item 0"
83
+ List.find_by_name('My other list').items.first_in_order.name.should == "My other list item 0"
84
+ end
85
+ end
86
+
87
+ describe "#last_in_order" do
88
+ it 'returns the last element of the list' do
89
+ List.find_by_name('My list').items.last_in_order.name.should == "My list item 3"
90
+ List.find_by_name('My other list').items.last_in_order.name.should == "My other list item 3"
91
+ end
92
+ end
93
+
94
+ describe "#ordered" do
95
+ it 'returns all elements ordered' do
96
+ List.find_by_name('My list').items.ordered.map(&:name).should == ['My list item 0', 'My list item 1', 'My list item 2', 'My list item 3']
97
+ List.find_by_name('My other list').items.ordered.map(&:name).should == ['My other list item 0', 'My other list item 1', 'My other list item 2', 'My other list item 3']
98
+ end
99
+
100
+ it 'raises when ordering without scope' do
101
+ expect {
102
+ ListItem.ordered
103
+ }.to raise_error
104
+ end
105
+ end
106
+
107
+ after do
108
+ List.destroy_all
109
+ ListItem.destroy_all
110
+ end
111
+ end
112
+
59
113
  describe "after create" do
60
114
  context 'when there are no siblings' do
61
115
  it 'prepends the element' do
@@ -72,14 +126,54 @@ module Resort
72
126
  Article.create(:name => 'last!')
73
127
 
74
128
  article = Article.find_by_name('last!')
129
+ first = Article.find_by_name('1')
75
130
 
76
131
  article.should be_last
132
+ article.next_id.should be_nil
77
133
  article.previous.name.should == '1'
134
+
135
+ first.next_id.should eq(article.id)
78
136
  end
79
137
  end
80
138
  after do
81
139
  Article.destroy_all
82
140
  end
141
+
142
+ context "with custom siblings" do
143
+
144
+ context 'when there are no siblings' do
145
+ it 'prepends the element' do
146
+ one_list = List.create(:name => 'My list')
147
+ another_list = List.create(:name => 'My other list')
148
+ item = ListItem.create(:name => "My list item", :list => one_list)
149
+
150
+ item.should be_first
151
+ item.next.should be_nil
152
+ item.previous.should be_nil
153
+ end
154
+ end
155
+ context 'otherwise' do
156
+ it 'appends the element' do
157
+ one_list = List.create(:name => 'My list')
158
+ another_list = List.create(:name => 'My other list')
159
+ ListItem.create(:name => "1", :list => one_list)
160
+ ListItem.create(:name => "last!", :list => one_list)
161
+
162
+ first = ListItem.find_by_name('1')
163
+ last = ListItem.find_by_name('last!')
164
+
165
+ last.should be_last
166
+ last.next_id.should be_nil
167
+ last.previous.name.should == '1'
168
+
169
+ first.next_id.should eq(last.id)
170
+ end
171
+ end
172
+ after do
173
+ List.destroy_all
174
+ ListItem.destroy_all
175
+ end
176
+ end
83
177
  end
84
178
 
85
179
  describe "after destroy" do
@@ -190,35 +284,43 @@ module Resort
190
284
  it "appends the element after another element" do
191
285
  @article1.append_to(@article2)
192
286
 
193
- article2 = Article.find_by_name('2')
194
- article2.next.name.should == '1'
195
-
196
287
  article1 = Article.find_by_name('1')
197
288
  article1.next.name.should == '3'
198
289
  article1.previous.name.should == '2'
199
290
  @article3.previous.name.should == '1'
291
+ end
292
+
293
+ it "sets the other element as first" do
294
+ @article1.append_to(@article2)
200
295
 
296
+ article2 = Article.find_by_name('2')
297
+ article2.next.name.should == '1'
201
298
  article2.should be_first
202
299
  end
203
300
  end
301
+
204
302
  context 'appending 1 after 3' do
205
303
  it "appends the element after another element" do
206
304
  @article1.append_to(@article3)
207
305
 
208
- article2 = Article.find_by_name('2')
209
- article2.should be_first
210
- article2.previous.should be_nil
211
-
212
306
  article1 = Article.find_by_name('1')
213
307
  article1.should_not be_first
214
308
  article1.previous.name.should == '3'
215
309
  article1.next.name.should == '4'
216
310
 
217
311
  @article3.next.name.should == '1'
218
-
219
312
  @article4.previous.name.should == '1'
220
313
  end
314
+
315
+ it 'resets the first element' do
316
+ @article1.append_to(@article3)
317
+
318
+ article2 = Article.find_by_name('2')
319
+ article2.should be_first
320
+ article2.previous.should be_nil
321
+ end
221
322
  end
323
+
222
324
  context 'appending 2 after 3' do
223
325
  it "appends the element after another element" do
224
326
  @article2.append_to(@article3)
@@ -290,8 +392,11 @@ module Resort
290
392
  it 'does nothing' do
291
393
  @article2.append_to(@article1)
292
394
 
293
- @article1.next.name.should == '2'
294
- @article2.previous.name.should == '1'
395
+ article1 = Article.find_by_name('1')
396
+ article2 = Article.find_by_name('2')
397
+
398
+ article1.next.name.should == '2'
399
+ article2.previous.name.should == '1'
295
400
  end
296
401
  end
297
402
  end
data/spec/spec_helper.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  require 'rspec'
2
2
  require 'resort'
3
+ require 'logger'
3
4
 
4
5
  ActiveRecord::Base.establish_connection(
5
6
  :adapter => 'sqlite3',
@@ -16,4 +17,38 @@ ActiveRecord::Schema.define do
16
17
 
17
18
  t.timestamps
18
19
  end
20
+
21
+ create_table :lists do |t|
22
+ t.string :name
23
+ t.timestamps
24
+ end
25
+
26
+ create_table :list_items do |t|
27
+ t.string :name
28
+ t.boolean :first
29
+ t.references :next
30
+ t.references :list
31
+ t.timestamps
32
+ end
33
+ end
34
+
35
+ # ActiveRecord::Base.logger = Logger.new(STDOUT)
36
+
37
+ class Article < ActiveRecord::Base
38
+ resort!
39
+ end
40
+
41
+ class List < ActiveRecord::Base
42
+ has_many :items, :class_name => 'ListItem'
43
+ end
44
+
45
+ class ListItem < ActiveRecord::Base
46
+ belongs_to :list
47
+ resort!
48
+
49
+ default_scope :order => 'created_at desc'
50
+
51
+ def siblings
52
+ self.list.items
53
+ end
19
54
  end
metadata CHANGED
@@ -2,7 +2,7 @@
2
2
  name: resort
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease:
5
- version: 0.0.2
5
+ version: 0.2.0
6
6
  platform: ruby
7
7
  authors:
8
8
  - Oriol Gual
@@ -12,7 +12,7 @@ autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
14
 
15
- date: 2011-03-24 00:00:00 +01:00
15
+ date: 2011-03-30 00:00:00 +02:00
16
16
  default_executable:
17
17
  dependencies:
18
18
  - !ruby/object:Gem::Dependency
@@ -70,6 +70,17 @@ dependencies:
70
70
  version: "0"
71
71
  type: :development
72
72
  version_requirements: *id005
73
+ - !ruby/object:Gem::Dependency
74
+ name: generator_spec
75
+ prerelease: false
76
+ requirement: &id006 !ruby/object:Gem::Requirement
77
+ none: false
78
+ requirements:
79
+ - - ~>
80
+ - !ruby/object:Gem::Version
81
+ version: 0.8.1
82
+ type: :development
83
+ version_requirements: *id006
73
84
  description: Positionless model sorting for Rails 3.
74
85
  email:
75
86
  - info@codegram.com
@@ -87,9 +98,12 @@ files:
87
98
  - LICENSE
88
99
  - Rakefile
89
100
  - Readme.md
101
+ - lib/generators/active_record/resort_generator.rb
102
+ - lib/generators/active_record/templates/migration.rb
90
103
  - lib/resort.rb
91
104
  - lib/resort/version.rb
92
105
  - resort.gemspec
106
+ - spec/generators/migration_spec.rb
93
107
  - spec/resort_spec.rb
94
108
  - spec/spec_helper.rb
95
109
  has_rdoc: true
@@ -121,5 +135,6 @@ signing_key:
121
135
  specification_version: 3
122
136
  summary: Positionless model sorting for Rails 3.
123
137
  test_files:
138
+ - spec/generators/migration_spec.rb
124
139
  - spec/resort_spec.rb
125
140
  - spec/spec_helper.rb