dm-is-list 0.9.2

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008 Sindre Aarsaether (somebee.com)
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.
data/README ADDED
@@ -0,0 +1,58 @@
1
+ dm-is-list
2
+ ==========
3
+
4
+ DataMapper plugin for creating and organizing lists.
5
+
6
+ == Installation
7
+
8
+ Download dm-more and install dm-is-list. Remember to require it in your app.
9
+
10
+ == Getting started
11
+
12
+ Lets say we have a user-class, and we want to give users the possibility of
13
+ having their own todo-lists
14
+
15
+ class Todo
16
+ include DataMapper::Resource
17
+
18
+ property :id, Serial
19
+ property :title, String
20
+ property :done, DateTime
21
+
22
+ belongs_to :user
23
+
24
+ # here we define that this should be a list, scoped on :user_id
25
+ is :list, :scope => [:user_id]
26
+ end
27
+
28
+ You can now move objects around like this:
29
+
30
+ item = Todo.get(1)
31
+ other = Todo.get(2)
32
+
33
+ item.move(:highest) # moves to top of list
34
+ item.move(:lowest) # moves to bottom of list
35
+ item.move(:up) # moves one up (:higher and :up is the same)
36
+ item.move(:down) # moves one up (:lower and :down is the same)
37
+ item.move(:to => position) # moves item to a specific position
38
+ item.move(:above => other) # moves item above the other item.*
39
+ item.move(:below => other) # moves item above the other item.*
40
+
41
+ * won't move if the other item is in another scope. (should this be allowed?)
42
+
43
+ The list will try to act as intelligently as possible. If you set the position
44
+ manually, and then save, the list will reorganize itself to correctly:
45
+
46
+ item.position = 3 # setting position manually
47
+ item.save # the list will now move the item correctly, and updating others
48
+
49
+ If you move items between scopes, the list will also try to do what you most
50
+ likely want to do:
51
+
52
+ item.user_id # => 1
53
+ item.user_id = 2 # giving this item to another user
54
+ item.save # the list will now have detached item from old list, and inserted
55
+ # at the bottom of the new (user 2) list.
56
+
57
+ If something is not behaving intuitively, it is a bug, and should be reported.
58
+ Report it here: http://wm.lighthouseapp.com/projects/4819-datamapper/overview
data/Rakefile ADDED
@@ -0,0 +1,56 @@
1
+ require 'rubygems'
2
+ require 'spec'
3
+ require 'rake/clean'
4
+ require 'rake/gempackagetask'
5
+ require 'spec/rake/spectask'
6
+ require 'pathname'
7
+
8
+ CLEAN.include '{log,pkg}/'
9
+
10
+ spec = Gem::Specification.new do |s|
11
+ s.name = 'dm-is-list'
12
+ s.version = '0.9.2'
13
+ s.platform = Gem::Platform::RUBY
14
+ s.has_rdoc = true
15
+ s.extra_rdoc_files = %w[ README LICENSE TODO ]
16
+ s.summary = 'DataMapper plugin for creating and organizing lists'
17
+ s.description = s.summary
18
+ s.author = 'Sindre Aarsaether'
19
+ s.email = 'sindre [a] identu [d] no'
20
+ s.homepage = 'http://github.com/sam/dm-more/tree/master/dm-is-list'
21
+ s.require_path = 'lib'
22
+ s.files = FileList[ '{lib,spec}/**/*.rb', 'spec/spec.opts', 'Rakefile', *s.extra_rdoc_files ]
23
+ s.add_dependency('dm-core', "=#{s.version}")
24
+ end
25
+
26
+ task :default => [ :spec ]
27
+
28
+ WIN32 = (RUBY_PLATFORM =~ /win32|mingw|cygwin/) rescue nil
29
+ SUDO = WIN32 ? '' : ('sudo' unless ENV['SUDOLESS'])
30
+
31
+ Rake::GemPackageTask.new(spec) do |pkg|
32
+ pkg.gem_spec = spec
33
+ end
34
+
35
+ desc "Install #{spec.name} #{spec.version} (default ruby)"
36
+ task :install => [ :package ] do
37
+ sh "#{SUDO} gem install --local pkg/#{spec.name}-#{spec.version} --no-update-sources", :verbose => false
38
+ end
39
+
40
+ desc "Uninstall #{spec.name} #{spec.version} (default ruby)"
41
+ task :uninstall => [ :clobber ] do
42
+ sh "#{SUDO} gem uninstall #{spec.name} -v#{spec.version} -I -x", :verbose => false
43
+ end
44
+
45
+ namespace :jruby do
46
+ desc "Install #{spec.name} #{spec.version} with JRuby"
47
+ task :install => [ :package ] do
48
+ sh %{#{SUDO} jruby -S gem install --local pkg/#{spec.name}-#{spec.version} --no-update-sources}, :verbose => false
49
+ end
50
+ end
51
+
52
+ desc 'Run specifications'
53
+ Spec::Rake::SpecTask.new(:spec) do |t|
54
+ t.spec_opts << '--options' << 'spec/spec.opts' if File.exists?('spec/spec.opts')
55
+ t.spec_files = Pathname.glob(Pathname.new(__FILE__).dirname + 'spec/**/*_spec.rb')
56
+ end
data/TODO ADDED
@@ -0,0 +1,3 @@
1
+ TODO
2
+ ====
3
+ * Add support for scopes
data/lib/dm-is-list.rb ADDED
@@ -0,0 +1,13 @@
1
+
2
+ require 'rubygems'
3
+ require 'pathname'
4
+
5
+ gem 'dm-core', '=0.9.2'
6
+ require 'dm-core'
7
+
8
+ gem 'dm-adjust', '=0.9.2'
9
+ require 'dm-adjust'
10
+
11
+ require Pathname(__FILE__).dirname.expand_path / 'dm-is-list' / 'is' / 'list.rb'
12
+
13
+ DataMapper::Model.append_extensions DataMapper::Is::List
@@ -0,0 +1,195 @@
1
+ module DataMapper
2
+ module Is
3
+ module List
4
+
5
+ ##
6
+ # method for making your model a list.
7
+ # it will define a :position property if it does not exist, so be sure to have a
8
+ # position-column in your database (will be added automatically on auto_migrate)
9
+ # if the column has a different name, simply make a :position-property and set a
10
+ # custom :field
11
+ #
12
+ # @example [Usage]
13
+ # is :list # put this in your model to make it act as a list.
14
+ # is :list, :scope => [:user_id] # you can also define scopes
15
+ # is :list, :scope => [:user_id, :context_id] # also works with multiple params
16
+ #
17
+ # @param options <Hash> a hash of options
18
+ #
19
+ # @option :scope<Array> an array of attributes that should be used to scope lists
20
+ #
21
+ def is_list(options={})
22
+ options = { :scope => [] }.merge(options)
23
+
24
+ extend DataMapper::Is::List::ClassMethods
25
+ include DataMapper::Is::List::InstanceMethods
26
+
27
+ property :position, Integer unless properties.detect{|p| p.name == :position && p.type == Integer}
28
+
29
+ @list_scope = options[:scope]
30
+
31
+ before :save do
32
+ if self.new_record?
33
+ # a position has been set before save => open up and make room for item
34
+ # no position has been set => move to bottom of my scope-list (or keep detached?)
35
+ self.position ? self.move_without_saving(:to => self.position) : self.move_without_saving(:lowest)
36
+ else
37
+ # if the scope has changed, we need to detach our item from the old list
38
+ if self.list_scope != self.original_list_scope
39
+ oldpos = self.original_values[:position]
40
+ newpos = self.position
41
+
42
+ self.detach(self.original_list_scope) # removing from old list
43
+ self.move_without_saving(oldpos ? {:to => newpos} : :lowest) # moving to pos or bottom of new list
44
+
45
+ elsif self.attribute_dirty?(:position)
46
+ self.move_without_saving(:to => self.position)
47
+ end
48
+ # a (new) position has been set => move item to this position (only if position has been set manually)
49
+ # the scope has changed => detach from old list, and possibly move into position
50
+ # the scope and position has changed => detach from old, move to pos in new
51
+ end
52
+ end
53
+
54
+ before :destroy do
55
+ self.detach
56
+ end
57
+
58
+ # we need to make sure that STI-models will inherit the list_scope.
59
+ after_class_method :inherited do |target|
60
+ target.instance_variable_set(:@list_scope, @list_scope.dup)
61
+ end
62
+
63
+ end
64
+
65
+ module ClassMethods
66
+ attr_reader :list_scope
67
+
68
+ ##
69
+ # use this function to repair / build your lists.
70
+ #
71
+ # @example [Usage]
72
+ # MyModel.repair_list # repairs the list, given that lists are not scoped
73
+ # MyModel.repair_list(:user_id => 1) # fixes the list for user 1, given that the scope is [:user_id]
74
+ #
75
+ # @param scope [Hash]
76
+ #
77
+ def repair_list(scope={})
78
+ return false unless scope.keys.all?{|s| list_scope.include?(s) || s == :order }
79
+ all({:order => [:position.asc]}.merge(scope)).each_with_index{ |item,i| item.position = i+1; item.save }
80
+ end
81
+ end
82
+
83
+ module InstanceMethods
84
+
85
+ def list_scope
86
+ self.class.list_scope.map{|p| [p,attribute_get(p)]}.to_hash
87
+ end
88
+
89
+ def original_list_scope
90
+ self.class.list_scope.map{|p| [p,original_values.key?(p) ? original_values[p] : attribute_get(p)]}.to_hash
91
+ end
92
+
93
+ def list_query
94
+ list_scope.merge(:order => [:position.asc])
95
+ end
96
+
97
+ def list(scope=list_query)
98
+ self.class.all(scope)
99
+ end
100
+
101
+ ##
102
+ # repair the list this item belongs to
103
+ #
104
+ def repair_list
105
+ self.class.repair_list(list_scope)
106
+ end
107
+
108
+ ##
109
+ # reorder the list this item belongs to
110
+ #
111
+ def reorder_list(order)
112
+ self.class.repair_list(list_scope.merge(:order => order))
113
+ end
114
+
115
+ ##
116
+ # move item to a position in the list. position should _only_ be changed through this
117
+ #
118
+ # @example [Usage]
119
+ # * node.move :higher # moves node higher unless it is at the top of parent
120
+ # * node.move :lower # moves node lower unless it is at the bottom of parent
121
+ # * node.move :below => other # moves this node below other resource in the set
122
+ #
123
+ # @param vector <Symbol, Hash> A symbol, or a key-value pair that describes the requested movement
124
+ #
125
+ # @option :higher<Symbol> move item higher
126
+ # @option :up<Symbol> move item higher
127
+ # @option :highest<Symbol> move item to the top of the list
128
+ # @option :lower<Symbol> move item lower
129
+ # @option :down<Symbol> move item lower
130
+ # @option :lowest<Symbol> move item to the bottom of the list
131
+ # @option :above<Resource> move item above other item. must be in same scope
132
+ # @option :below<Resource> move item below other item. must be in same scope
133
+ # @option :to<Fixnum> move item to a specific location in the list
134
+ #
135
+ # @return <TrueClass, FalseClass> returns false if it cannot move to the position, otherwise true
136
+ # @see move_without_saving
137
+ def move(vector)
138
+ move_without_saving(vector)
139
+ save
140
+ end
141
+
142
+ ##
143
+ # does all the actual movement in #move, but does not save afterwards. this is used internally in
144
+ # before :save, and will probably be marked private. should not be used by organic beings.
145
+ #
146
+ # @see move_without_saving
147
+ def move_without_saving(vector)
148
+ if vector.is_a? Hash then action,object = vector.keys[0],vector.values[0] else action = vector end
149
+
150
+ prepos = self.original_values[:position]||self.position
151
+ maxpos = list.last ? (list.last == self ? prepos : list.last.position + 1) : 1
152
+ newpos = case action
153
+ when :highest then 1
154
+ when :lowest then maxpos
155
+ when :higher,:up then [position-1,1].max
156
+ when :lower,:down then [position+1,maxpos].min
157
+ when :above then object.position
158
+ when :below then object.position+1
159
+ when :to then [object.to_i,maxpos].min
160
+ end
161
+
162
+ return false if !newpos || ([:above,:below].include?(action) && list_scope != object.list_scope)
163
+ return true if newpos == position || (newpos == maxpos && position == maxpos-1)
164
+
165
+ if !position
166
+ list.all(:position.gte => newpos).adjust!({:position => +1},true) unless action == :lowest
167
+ elsif newpos > position
168
+ newpos -= 1 if [:lowest,:above,:below,:to].include?(action)
169
+ list.all(:position => position..newpos).adjust!({:position => -1},true)
170
+ elsif newpos < position
171
+ list.all(:position => newpos..position).adjust!({:position => +1},true)
172
+ end
173
+
174
+ self.position = newpos
175
+
176
+ true
177
+ end
178
+
179
+ def detach(scope=list_scope)
180
+ list(scope).all(:position.gt => position).adjust!({:position => -1},true)
181
+ self.position = nil
182
+ end
183
+
184
+ def left_sibling
185
+ list.reverse.first(:position.lt => position)
186
+ end
187
+
188
+ def right_sibling
189
+ list.first(:position.gt => position)
190
+ end
191
+
192
+ end
193
+ end # List
194
+ end # Is
195
+ end # DataMapper
@@ -0,0 +1,181 @@
1
+ require 'pathname'
2
+ require Pathname(__FILE__).dirname.expand_path.parent + 'spec_helper'
3
+
4
+ if HAS_SQLITE3 || HAS_MYSQL || HAS_POSTGRES
5
+ describe 'DataMapper::Is::List' do
6
+
7
+ class User
8
+ include DataMapper::Resource
9
+
10
+ property :id, Serial
11
+ property :name, String
12
+
13
+ has n, :todos
14
+ end
15
+
16
+ class Todo
17
+ include DataMapper::Resource
18
+
19
+ property :id, Serial
20
+ property :title, String
21
+
22
+ belongs_to :user
23
+
24
+ is :list, :scope => [:user_id]
25
+ end
26
+
27
+ before :all do
28
+ User.auto_migrate!(:default)
29
+ Todo.auto_migrate!(:default)
30
+
31
+ u1 = User.create!(:name => "Johnny")
32
+ Todo.create!(:user => u1, :title => "Write down what is needed in a list-plugin")
33
+ Todo.create!(:user => u1, :title => "Complete a temporary version of is-list")
34
+ Todo.create!(:user => u1, :title => "Squash bugs in nested-set")
35
+
36
+ u2 = User.create!(:name => "Freddy")
37
+ Todo.create!(:user => u2, :title => "Eat tasty cupcake")
38
+ Todo.create!(:user => u2, :title => "Procrastinate on paid work")
39
+ Todo.create!(:user => u2, :title => "Go to sleep")
40
+
41
+ end
42
+
43
+ describe 'automatic positioning' do
44
+ it 'should get the shadow variable of the last position' do
45
+ repository(:default) do |repos|
46
+ Todo.get(3).position=8
47
+ Todo.get(3).dirty?.should == true
48
+ Todo.get(3).attribute_dirty?(:position).should == true
49
+ Todo.get(3).original_values[:position].should == 3
50
+ Todo.get(3).list_scope.should == Todo.get(3).original_list_scope
51
+ end
52
+ end
53
+
54
+ it 'should insert items into the list automatically' do
55
+ repository(:default) do |repos|
56
+ Todo.get(3).position.should == 3
57
+ Todo.get(6).position.should == 3
58
+ end
59
+ end
60
+ end
61
+
62
+ describe 'movement' do
63
+ it 'should rearrange items correctly when moving :higher' do
64
+ repository(:default) do |repos|
65
+ Todo.get(3).move :higher
66
+ Todo.get(3).position.should == 2
67
+ Todo.get(2).position.should == 3
68
+ Todo.get(4).position.should == 1
69
+ end
70
+ end
71
+
72
+ it 'should rearrange items correctly when moving :lower' do
73
+ repository(:default) do |repos|
74
+ Todo.get(3).position.should == 2
75
+ Todo.get(2).position.should == 3
76
+ Todo.get(3).move :lower
77
+ Todo.get(3).position.should == 3
78
+ Todo.get(2).position.should == 2
79
+
80
+ Todo.get(4).position.should == 1
81
+ end
82
+ end
83
+
84
+ it 'should rearrange items correctly when moving :highest or :lowest' do
85
+ repository(:default) do |repos|
86
+
87
+ # list 1
88
+ Todo.get(1).position.should == 1
89
+ Todo.get(1).move(:lowest)
90
+ Todo.get(1).position.should == 3
91
+
92
+ # list 2
93
+ Todo.get(6).position.should == 3
94
+ Todo.get(6).move(:highest)
95
+ Todo.get(6).position.should == 1
96
+ Todo.get(5).position.should == 3
97
+ end
98
+ end
99
+
100
+ it 'should not rearrange when trying to move top-item up, or bottom item down' do
101
+ repository(:default) do |repos|
102
+ Todo.get(6).position.should == 1
103
+ Todo.get(6).move(:higher).should == false
104
+ Todo.get(6).position.should == 1
105
+
106
+ Todo.get(1).position.should == 3
107
+ Todo.get(2).position.should == 1
108
+ Todo.get(1).move(:lower).should == false
109
+ end
110
+ end
111
+
112
+ it 'should rearrange items correctly when moving :above or :below' do
113
+ repository(:default) do |repos|
114
+ Todo.get(6).position.should == 1
115
+ Todo.get(5).position.should == 3
116
+ Todo.get(6).move(:below => Todo.get(5))
117
+ Todo.get(6).position.should == 3
118
+ Todo.get(5).position.should == 2
119
+ end
120
+ end
121
+ end
122
+
123
+ describe 'scoping' do
124
+ it 'should detach from old list if scope changed' do
125
+ repository(:default) do |repos|
126
+ item = Todo.get(4)
127
+ item.position.should == 1
128
+ item.user_id = 1
129
+ item.save
130
+
131
+ item.position.should == 4
132
+ Todo.get(5).position.should == 1
133
+
134
+ item.user_id = 2
135
+ item.position = 1
136
+ item.save
137
+
138
+ item.position.should == 1
139
+ Todo.get(5).position.should == 2
140
+
141
+ end
142
+ end
143
+
144
+ it 'should not allow you to move item into another scope' do
145
+ repository(:default) do |repos|
146
+ item = Todo.get(1)
147
+ item.position.should == 1
148
+ item.move(:below => Todo.get(5)).should == false
149
+ end
150
+ end
151
+
152
+ it 'should detach from list when deleted' do
153
+ repository(:default) do |repos|
154
+ item = Todo.get(4)
155
+ item.position.should == 1
156
+ Todo.get(5).position.should == 2
157
+ item.destroy
158
+
159
+ Todo.get(5).position.should == 1
160
+
161
+ end
162
+ end
163
+ end
164
+
165
+ describe 'reparation' do
166
+ it 'should fix them lists' do
167
+ repository(:default) do |repos|
168
+ # Need to do this with a batch update, as setting position = 20 wont
169
+ # work (it will set it to bottom of list, not more)
170
+ Todo.all(:position => 3).update!(:position => 20)
171
+
172
+ item = Todo.get(6)
173
+ item.position.should == 20
174
+ item.repair_list
175
+ item.position.should == 3
176
+ end
177
+ end
178
+ end
179
+
180
+ end
181
+ end
data/spec/spec.opts ADDED
@@ -0,0 +1,2 @@
1
+ --format specdoc
2
+ --colour
@@ -0,0 +1,28 @@
1
+ require 'rubygems'
2
+ gem 'rspec', '>=1.1.3'
3
+ require 'spec'
4
+ require 'pathname'
5
+ require Pathname(__FILE__).dirname.expand_path.parent + 'lib/dm-is-list'
6
+
7
+ def load_driver(name, default_uri)
8
+ return false if ENV['ADAPTER'] != name.to_s
9
+
10
+ lib = "do_#{name}"
11
+
12
+ begin
13
+ gem lib, '=0.9.2'
14
+ require lib
15
+ DataMapper.setup(name, ENV["#{name.to_s.upcase}_SPEC_URI"] || default_uri)
16
+ DataMapper::Repository.adapters[:default] = DataMapper::Repository.adapters[name]
17
+ true
18
+ rescue Gem::LoadError => e
19
+ warn "Could not load #{lib}: #{e}"
20
+ false
21
+ end
22
+ end
23
+
24
+ ENV['ADAPTER'] ||= 'sqlite3'
25
+
26
+ HAS_SQLITE3 = load_driver(:sqlite3, 'sqlite3::memory:')
27
+ HAS_MYSQL = load_driver(:mysql, 'mysql://localhost/dm_core_test')
28
+ HAS_POSTGRES = load_driver(:postgres, 'postgres://postgres@localhost/dm_core_test')
metadata ADDED
@@ -0,0 +1,71 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dm-is-list
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.9.2
5
+ platform: ruby
6
+ authors:
7
+ - Sindre Aarsaether
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-06-25 00:00:00 -05:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: dm-core
17
+ version_requirement:
18
+ version_requirements: !ruby/object:Gem::Requirement
19
+ requirements:
20
+ - - "="
21
+ - !ruby/object:Gem::Version
22
+ version: 0.9.2
23
+ version:
24
+ description: DataMapper plugin for creating and organizing lists
25
+ email: sindre [a] identu [d] no
26
+ executables: []
27
+
28
+ extensions: []
29
+
30
+ extra_rdoc_files:
31
+ - README
32
+ - LICENSE
33
+ - TODO
34
+ files:
35
+ - lib/dm-is-list/is/list.rb
36
+ - lib/dm-is-list.rb
37
+ - spec/integration/list_spec.rb
38
+ - spec/spec_helper.rb
39
+ - spec/spec.opts
40
+ - Rakefile
41
+ - README
42
+ - LICENSE
43
+ - TODO
44
+ has_rdoc: true
45
+ homepage: http://github.com/sam/dm-more/tree/master/dm-is-list
46
+ post_install_message:
47
+ rdoc_options: []
48
+
49
+ require_paths:
50
+ - lib
51
+ required_ruby_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: "0"
56
+ version:
57
+ required_rubygems_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: "0"
62
+ version:
63
+ requirements: []
64
+
65
+ rubyforge_project:
66
+ rubygems_version: 1.0.1
67
+ signing_key:
68
+ specification_version: 2
69
+ summary: DataMapper plugin for creating and organizing lists
70
+ test_files: []
71
+