mm_sortable_item 0.0.1

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 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