resort 0.0.2 → 0.2.0

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