mm_sortable_item 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --colour
2
+ --format d
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm use --create 1.9.2@mm_sortable_item_gem
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in mm_sortable_item.gemspec
4
+ gemspec
data/README.md ADDED
@@ -0,0 +1,36 @@
1
+ # mm\_sortable\_item
2
+
3
+ This is a quick little MongoMapper plugin that provides some basic acts-as-list style functionality on mongo documents. By default things are added to the list bottom.
4
+
5
+ ## Usage
6
+
7
+ In the gemfile:
8
+
9
+ gem 'mm_sortable_item'
10
+
11
+ In your model:
12
+
13
+ ``` ruby
14
+ class SortableDocument
15
+ include MongoMapper::Document
16
+ plugin MongoMapper::Plugins::SortableItem
17
+
18
+ list_scope_column :parent_id # optional if you want to scope the lists
19
+
20
+ # ...
21
+ end
22
+ ```
23
+
24
+ Then you have access to some helpful methods, such as:
25
+
26
+ * `.in_order` retrieves the list items in order
27
+ * `.in_list(id)` retrieves the items scoped as you wish
28
+ * `.reorder(orderd_array_of_ids)` sets the positions of the given ids in order
29
+ * `object.set_position(position)` inserts object into the list at the given position
30
+
31
+ ## Credit
32
+
33
+ John Nunemaker for mongo_mapper itself, as well as a start down the road of how to implement this. It's definitely not as "fully functional" as `acts_as_list`, but it does everything I need :).
34
+
35
+ * Author: Matt Wilson (mwilson@agoragames.com)
36
+ * GitHub: http://github.com/hypomodern
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require 'bundler/gem_tasks'
@@ -0,0 +1,111 @@
1
+ require "mm_sortable_item/version"
2
+ require "mongo_mapper"
3
+
4
+ # An ActsAsList-ish plugin for MongoMapper, since this doesn't seem to exist in a well-tested form.
5
+ # Props to John Nunemaker for starting us down the right path here
6
+ module MongoMapper
7
+ module Plugins
8
+ module SortableItem
9
+ extend ActiveSupport::Concern
10
+
11
+ included do
12
+ key :position, Integer
13
+ scope :in_order, sort(:position)
14
+ class_attribute :sortable_item_options
15
+ self.sortable_item_options = { :list_scope => nil }
16
+
17
+ # some callbacks we'll want
18
+ before_create :add_to_list_bottom, :unless => :in_list?
19
+ before_destroy :decrement_positions_on_lower_items
20
+ end
21
+
22
+ module ClassMethods
23
+ def reorder(ids)
24
+ ids.each_with_index do |id, index|
25
+ set(id, :position => index + 1)
26
+ end
27
+ end
28
+
29
+ def in_list list_id = nil
30
+ where(conditions_for_list_scope(list_id))
31
+ end
32
+
33
+ def conditions_for_list_scope list_id
34
+ the_query = {}
35
+ the_column = list_scope_column
36
+ if the_column
37
+ the_query = { the_column => list_id }
38
+ end
39
+ the_query
40
+ end
41
+
42
+ def list_scope_column= new_column
43
+ self.sortable_item_options[:list_scope] = new_column
44
+ end
45
+
46
+ def list_scope_column
47
+ self.sortable_item_options[:list_scope]
48
+ end
49
+ end
50
+
51
+ module InstanceMethods
52
+ def in_list?
53
+ !send(:position).nil?
54
+ end
55
+
56
+ def scoped_list_id
57
+ the_column = self.class.list_scope_column
58
+ the_column ? self.send(the_column) : nil
59
+ end
60
+
61
+ def add_to_list_bottom
62
+ add_to_list
63
+ end
64
+
65
+ def add_to_list_top
66
+ add_to_list 1
67
+ end
68
+
69
+ def add_to_list position = bottom_of_list
70
+ remove_from_list
71
+ increment_positions_on_lower_items position
72
+ set_position position
73
+ end
74
+
75
+ def bottom_of_list
76
+ self.class.in_list( scoped_list_id ).count + 1
77
+ end
78
+
79
+ def lower_than_conditions position = self.position
80
+ query = self.class.conditions_for_list_scope scoped_list_id
81
+ query.merge( :position.gte => position )
82
+ end
83
+
84
+ def decrement_positions_on_lower_items position = self.position
85
+ self.class.decrement( lower_than_conditions(position), { :position => 1 } )
86
+ end
87
+
88
+ def increment_positions_on_lower_items position = self.position
89
+ self.class.increment( lower_than_conditions(position), { :position => 1 } )
90
+ end
91
+
92
+ def remove_from_list
93
+ if in_list?
94
+ decrement_positions_on_lower_items
95
+ self.position = nil
96
+ end
97
+ end
98
+
99
+ def set_position new_position
100
+ remove_from_list
101
+ increment_positions_on_lower_items new_position
102
+ if new_position != self.position
103
+ self.position = new_position
104
+ save unless new_record?
105
+ end
106
+ end
107
+
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,3 @@
1
+ module MmSortableItem
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "mm_sortable_item/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "mm_sortable_item"
7
+ s.version = MmSortableItem::VERSION
8
+ s.authors = ["Matt Wilson"]
9
+ s.email = ["mhw@hypomodern.com"]
10
+ s.homepage = "https://github.com/agoragames/mm_sortable_item"
11
+ s.summary = "Tiny MongoMapper plugin for treating a collection as a list"
12
+ s.description = "Tiny MongoMapper plugin for treating a collection as a list"
13
+
14
+ s.rubyforge_project = "mm_sortable_item"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+
21
+ s.add_runtime_dependency('mongo_mapper')
22
+ s.add_development_dependency('rspec')
23
+ s.add_development_dependency('fabrication')
24
+ s.add_development_dependency('database_cleaner')
25
+ end
@@ -0,0 +1,7 @@
1
+ if !defined?(SortableHelper)
2
+ class SortableHelper; end
3
+ end
4
+ Fabricator(:sortable_helper) do
5
+ name { sequence(:name) { |i| "leet_sortable_#{i}" } }
6
+ list_scope_id { (1..3).to_a.sample }
7
+ end
@@ -0,0 +1,196 @@
1
+ require 'spec_helper'
2
+
3
+ module MongoMapper::Plugins
4
+ describe SortableItem do
5
+ after do
6
+ SortableHelper.sortable_item_options = { :list_scope => nil }
7
+ end
8
+
9
+ describe "plugin .included magic" do
10
+ it "sets a new mongo key of :position on the document" do
11
+ the_position_key = SortableHelper.keys["position"]
12
+ the_position_key.should_not be_nil
13
+
14
+ the_position_key.type.should == Integer
15
+ end
16
+ it "provides a new scope named .in_order" do
17
+ SortableHelper.scopes.keys.should include(:in_order)
18
+ end
19
+ it "sets up a class_accessor called .sortable_item_options" do
20
+ SortableHelper.sortable_item_options.should == { :list_scope => nil }
21
+ end
22
+
23
+ describe "default callbacks" do
24
+ it "sets up a before_create callback to ensure the item gets added to the list" do
25
+ the_callbacks = SortableHelper._create_callbacks
26
+ the_callbacks.find { |cb| cb.kind == :before && cb.filter == :add_to_list_bottom }.
27
+ should_not be_nil
28
+ end
29
+ it "adds new items to the bottom of the list by default" do
30
+ 3.times { Fabricate(:sortable_helper) }
31
+ new_item = Fabricate(:sortable_helper)
32
+ new_item.position.should == 4
33
+ end
34
+ context "with already-ordered items" do
35
+ it "doesn't change the position" do
36
+ (1..3).to_a.each { |i| Fabricate(:sortable_helper, :position => i + 1) }
37
+ new_item = Fabricate(:sortable_helper, :position => 1)
38
+ new_item.reload
39
+ new_item.position.should == 1
40
+ SortableHelper.in_order.first.should == new_item
41
+ end
42
+ end
43
+ it "sets up a before_destroy callback to ensure the item gets removed from the list" do
44
+ the_callbacks = SortableHelper._destroy_callbacks
45
+ the_callbacks.find { |cb| cb.kind == :before && cb.filter == :decrement_positions_on_lower_items }.
46
+ should_not be_nil
47
+ end
48
+ it "gracefully removes an item from the list, leaving it in proper order on destroy" do
49
+ (1..3).to_a.each { |i| Fabricate(:sortable_helper, :position => i) }
50
+ SortableHelper.in_order.first.destroy
51
+ SortableHelper.in_order.all.map { |sh| sh.position }.should == [1, 2]
52
+ end
53
+ end
54
+ end
55
+
56
+ describe ".list_scope_column=" do
57
+ it "allows you to give it a column name that will be saved into the options" do
58
+ SortableHelper.list_scope_column = :list_scope_id
59
+ SortableHelper.sortable_item_options.should == { :list_scope => :list_scope_id }
60
+ end
61
+ end
62
+
63
+ describe ".list_scope_column" do
64
+ it "returns the defined list_scope_column" do
65
+ SortableHelper.list_scope_column = :list_scope_id
66
+ SortableHelper.list_scope_column.should == :list_scope_id
67
+ end
68
+ it "returns nil by default" do
69
+ SortableHelper.list_scope_column.should be_nil
70
+ end
71
+ end
72
+
73
+ describe ".in_list" do
74
+ it "returns a scope that... uh, scopes the list" do
75
+ SortableHelper.in_list.should be_a_kind_of(Plucky::Query)
76
+ end
77
+ it "filters nothing by default (entire collection is the list)" do
78
+ 3.times { Fabricate(:sortable_helper) }
79
+ recs = SortableHelper.in_list.in_order.all
80
+ recs.size.should == 3
81
+ end
82
+ it "uses the :list_scope option to build the scope" do
83
+ SortableHelper.list_scope_column = :list_scope_id
84
+ SortableHelper.should_receive(:where).with({:list_scope_id => 3})
85
+ SortableHelper.in_list(3)
86
+ end
87
+ it "correctly scopes the list, baby" do
88
+ 5.times { Fabricate(:sortable_helper, :list_scope_id => 1) }
89
+ 5.times { Fabricate(:sortable_helper, :list_scope_id => 2) }
90
+ SortableHelper.list_scope_column = :list_scope_id
91
+
92
+ SortableHelper.in_list(1).count.should == 5
93
+ SortableHelper.in_list(2).count.should == 5
94
+ SortableHelper.count.should == 10
95
+ end
96
+ end
97
+
98
+ describe ".reorder" do
99
+ it "updates the positions of the given ids based on their array order" do
100
+ items = (1..3).to_a.map { |i| Fabricate(:sortable_helper, :position => i) }
101
+
102
+ SortableHelper.reorder([items[2].id, items[0].id, items[1].id])
103
+ new_list = SortableHelper.in_list.in_order.all
104
+ new_list[0].should == items[2]
105
+ new_list[1].should == items[0]
106
+ new_list[2].should == items[1]
107
+ end
108
+ end
109
+
110
+ describe "#in_list?" do
111
+ it "returns false if the record doesn't have a numeric position" do
112
+ sortable = Fabricate.build(:sortable_helper)
113
+ sortable.stub!(:position).and_return(nil)
114
+ sortable.position.should be_nil
115
+ sortable.should_not be_in_list
116
+ end
117
+ it "returns true if the record has a numeric position" do
118
+ sortable = Fabricate.build(:sortable_helper)
119
+ sortable.position = 1
120
+ sortable.should be_in_list
121
+ end
122
+ end
123
+
124
+ describe "#scoped_list_id" do
125
+ it "returns nil if there is no defined list scope" do
126
+ Fabricate.build(:sortable_helper).scoped_list_id.should be_nil
127
+ end
128
+ it "returns the value of the given column" do
129
+ SortableHelper.list_scope_column = :list_scope_id
130
+ sortable = Fabricate.build(:sortable_helper)
131
+ sortable.scoped_list_id.should == sortable.list_scope_id
132
+ end
133
+ end
134
+
135
+ describe "#bottom_of_list" do
136
+ before do
137
+ SortableHelper.list_scope_column = :list_scope_id
138
+ 5.times { Fabricate(:sortable_helper, :list_scope_id => 1) }
139
+ 2.times { Fabricate(:sortable_helper, :list_scope_id => 2) }
140
+ end
141
+ it "returns the position that an item at the bottom of the list should have" do
142
+ list_1 = Fabricate.build(:sortable_helper, :list_scope_id => 1)
143
+ list_2 = Fabricate.build(:sortable_helper, :list_scope_id => 2)
144
+ list_1.bottom_of_list.should == 6
145
+ list_2.bottom_of_list.should == 3
146
+ end
147
+ end
148
+
149
+ describe "#remove_from_list" do
150
+ before do
151
+ @list = (1..5).to_a.map { |i| Fabricate(:sortable_helper, :name => "Item #{i}") }
152
+ @middle_item = @list[2]
153
+ end
154
+ it "sets the current position to nil" do
155
+ @middle_item.remove_from_list
156
+ @middle_item.position.should be_nil
157
+ end
158
+ it "pushes everything below the current item up a notch" do
159
+ old_position = @middle_item.position
160
+ @middle_item.remove_from_list
161
+ item_3 = @list[3]
162
+ item_3.reload
163
+ item_3.position.should == old_position
164
+
165
+ item_4 = @list[4]
166
+ item_4.reload
167
+ item_4.position.should == old_position + 1
168
+ end
169
+ end
170
+
171
+ describe "#set_position" do
172
+ it "sets the item to the new position" do
173
+ item = Fabricate.build(:sortable_helper)
174
+ item.set_position 5
175
+ item.position.should == 5
176
+ end
177
+ it "moves everything below the given position down a notch" do
178
+ list = (1..5).to_a.map { |i| Fabricate(:sortable_helper, :name => "Item #{i}") }
179
+ item = Fabricate(:sortable_helper)
180
+ item.set_position 3
181
+ old_item_3 = SortableHelper.find(list[2].id)
182
+ old_item_3.reload
183
+ old_item_3.position.should == 4
184
+ end
185
+ it "inserts it correctly into the list" do
186
+ list = (1..5).to_a.map { |i| Fabricate(:sortable_helper, :name => "Item #{i}") }
187
+ item = Fabricate(:sortable_helper, :name => "The New Guy")
188
+ item.set_position 3
189
+ new_list = SortableHelper.in_list.in_order.all
190
+ new_list[2].should == item
191
+ new_list.map { |i| i.name + ": pos = " + i.position.to_s }.should == ["Item 1: pos = 1", "Item 2: pos = 2", "The New Guy: pos = 3", "Item 3: pos = 4", "Item 4: pos = 5", "Item 5: pos = 6"]
192
+ end
193
+ end
194
+
195
+ end
196
+ end
@@ -0,0 +1,42 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
2
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
3
+
4
+ require 'rubygems'
5
+ require 'bundler'
6
+ Bundler.setup
7
+ require 'rspec'
8
+ require 'fabrication'
9
+ require 'database_cleaner'
10
+
11
+
12
+ require 'mm_sortable_item'
13
+
14
+ MongoMapper.database = 'mm_sortable_item_spec'
15
+
16
+ class SortableHelper
17
+ include MongoMapper::Document
18
+ plugin MongoMapper::Plugins::SortableItem
19
+
20
+ key :name, String
21
+ key :list_scope_id, Integer
22
+ end
23
+
24
+ SortableHelper.collection.remove
25
+
26
+ RSpec.configure do |config|
27
+ config.mock_with :rspec
28
+
29
+ config.before(:suite) do
30
+ DatabaseCleaner.strategy = :truncation
31
+ DatabaseCleaner.clean_with(:truncation)
32
+ end
33
+
34
+ config.before(:each) do
35
+ DatabaseCleaner.start
36
+ DatabaseCleaner.clean
37
+ end
38
+
39
+ config.after(:each) do
40
+ DatabaseCleaner.clean
41
+ end
42
+ end
metadata ADDED
@@ -0,0 +1,113 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mm_sortable_item
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.0.1
6
+ platform: ruby
7
+ authors:
8
+ - Matt Wilson
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2011-07-31 00:00:00 -04:00
14
+ default_executable:
15
+ dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: mongo_mapper
18
+ prerelease: false
19
+ requirement: &id001 !ruby/object:Gem::Requirement
20
+ none: false
21
+ requirements:
22
+ - - ">="
23
+ - !ruby/object:Gem::Version
24
+ version: "0"
25
+ type: :runtime
26
+ version_requirements: *id001
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ prerelease: false
30
+ requirement: &id002 !ruby/object:Gem::Requirement
31
+ none: false
32
+ requirements:
33
+ - - ">="
34
+ - !ruby/object:Gem::Version
35
+ version: "0"
36
+ type: :development
37
+ version_requirements: *id002
38
+ - !ruby/object:Gem::Dependency
39
+ name: fabrication
40
+ prerelease: false
41
+ requirement: &id003 !ruby/object:Gem::Requirement
42
+ none: false
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: "0"
47
+ type: :development
48
+ version_requirements: *id003
49
+ - !ruby/object:Gem::Dependency
50
+ name: database_cleaner
51
+ prerelease: false
52
+ requirement: &id004 !ruby/object:Gem::Requirement
53
+ none: false
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: "0"
58
+ type: :development
59
+ version_requirements: *id004
60
+ description: Tiny MongoMapper plugin for treating a collection as a list
61
+ email:
62
+ - mhw@hypomodern.com
63
+ executables: []
64
+
65
+ extensions: []
66
+
67
+ extra_rdoc_files: []
68
+
69
+ files:
70
+ - .gitignore
71
+ - .rspec
72
+ - .rvmrc
73
+ - Gemfile
74
+ - README.md
75
+ - Rakefile
76
+ - lib/mm_sortable_item.rb
77
+ - lib/mm_sortable_item/version.rb
78
+ - mm_sortable_item.gemspec
79
+ - spec/fabricators/sortable_helper_fabricator.rb
80
+ - spec/mm_sortable_item_spec.rb
81
+ - spec/spec_helper.rb
82
+ has_rdoc: true
83
+ homepage: https://github.com/agoragames/mm_sortable_item
84
+ licenses: []
85
+
86
+ post_install_message:
87
+ rdoc_options: []
88
+
89
+ require_paths:
90
+ - lib
91
+ required_ruby_version: !ruby/object:Gem::Requirement
92
+ none: false
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: "0"
97
+ required_rubygems_version: !ruby/object:Gem::Requirement
98
+ none: false
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: "0"
103
+ requirements: []
104
+
105
+ rubyforge_project: mm_sortable_item
106
+ rubygems_version: 1.6.2
107
+ signing_key:
108
+ specification_version: 3
109
+ summary: Tiny MongoMapper plugin for treating a collection as a list
110
+ test_files:
111
+ - spec/fabricators/sortable_helper_fabricator.rb
112
+ - spec/mm_sortable_item_spec.rb
113
+ - spec/spec_helper.rb