dm-is-list 0.9.2

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