dm-is-awesome_set 0.10.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/.gitignore +2 -0
- data/LICENSE +23 -0
- data/README +16 -0
- data/Rakefile +33 -0
- data/TODO +4 -0
- data/VERSION +1 -0
- data/dm-is-awesome_set.gemspec +65 -0
- data/lib/dm-is-awesome_set.rb +5 -0
- data/lib/dm-is-awesome_set/is/awesome_set.rb +428 -0
- data/spec/dm-is-awesome_set_spec.rb +650 -0
- data/spec/spec_helper.rb +105 -0
- metadata +109 -0
data/.gitignore
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
Copyright (c) 2008 Jeremy Nicoll (http://gnexp.com, jnicoll@gnexp.com)
|
2
|
+
|
3
|
+
Some code and documentation janked from dm-is-nested_set by Sindre Aarsaether
|
4
|
+
(somebee.com)
|
5
|
+
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
7
|
+
a copy of this software and associated documentation files (the
|
8
|
+
"Software"), to deal in the Software without restriction, including
|
9
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
10
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
11
|
+
permit persons to whom the Software is furnished to do so, subject to
|
12
|
+
the following conditions:
|
13
|
+
|
14
|
+
The above copyright notice and this permission notice shall be
|
15
|
+
included in all copies or substantial portions of the Software
|
16
|
+
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
18
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
19
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
20
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
21
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
22
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
23
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
dm-is-awesome_set
|
2
|
+
=================
|
3
|
+
|
4
|
+
Yes, finally! A nested_set for Datamapper that actually works! .... at least
|
5
|
+
I _think_ it does. Please test this out and let me know at jnicoll@gnexp.com
|
6
|
+
if you run into any problems. This readme will eventually have examples. Until
|
7
|
+
then, check the RDoc's and you should be fine.
|
8
|
+
|
9
|
+
A quick note about discriminators:
|
10
|
+
|
11
|
+
This version enables scoping that can either include or ignore discriminators.
|
12
|
+
If you wish to scope by a discriminator, please include that column name in
|
13
|
+
the scope option. Otherwise this plugin will work with all rows regardless of
|
14
|
+
the discriminator column.
|
15
|
+
|
16
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
|
6
|
+
gem 'jeweler', '>= 1.4'
|
7
|
+
require 'jeweler'
|
8
|
+
|
9
|
+
Jeweler::Tasks.new do |gem|
|
10
|
+
|
11
|
+
gem.name = "dm-is-awesome_set"
|
12
|
+
gem.summary = %Q{A nested set plugin for datamapper}
|
13
|
+
gem.description = %Q{A library that lets any datamapper model act like a nested set}
|
14
|
+
gem.email = "jnicoll@gnexp.com"
|
15
|
+
gem.homepage = "http://github.com/snusnu/dm-is-awesome_set"
|
16
|
+
gem.authors = ["Jeremy Nicoll", "David Haslem", "Martin Gamsjaeger (snusnu)"]
|
17
|
+
|
18
|
+
gem.add_dependency 'dm-core', '~> 0.10'
|
19
|
+
gem.add_dependency 'dm-adjust', '~> 0.10'
|
20
|
+
gem.add_dependency 'dm-aggregates', '~> 0.10'
|
21
|
+
|
22
|
+
gem.add_development_dependency 'rspec', '~> 1.2.9'
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
Jeweler::GemcutterTasks.new
|
27
|
+
|
28
|
+
rescue LoadError
|
29
|
+
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
30
|
+
end
|
31
|
+
|
32
|
+
task :spec => :check_dependencies
|
33
|
+
task :default => :spec
|
data/TODO
ADDED
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.10.2
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{dm-is-awesome_set}
|
8
|
+
s.version = "0.10.2"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Jeremy Nicoll", "David Haslem", "Martin Gamsjaeger (snusnu)"]
|
12
|
+
s.date = %q{2010-02-19}
|
13
|
+
s.description = %q{A library that lets any datamapper model act like a nested set}
|
14
|
+
s.email = %q{jnicoll@gnexp.com}
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"LICENSE",
|
17
|
+
"README",
|
18
|
+
"TODO"
|
19
|
+
]
|
20
|
+
s.files = [
|
21
|
+
".gitignore",
|
22
|
+
"LICENSE",
|
23
|
+
"README",
|
24
|
+
"Rakefile",
|
25
|
+
"TODO",
|
26
|
+
"VERSION",
|
27
|
+
"dm-is-awesome_set.gemspec",
|
28
|
+
"lib/dm-is-awesome_set.rb",
|
29
|
+
"lib/dm-is-awesome_set/is/awesome_set.rb",
|
30
|
+
"spec/dm-is-awesome_set_spec.rb",
|
31
|
+
"spec/spec_helper.rb"
|
32
|
+
]
|
33
|
+
s.homepage = %q{http://github.com/snusnu/dm-is-awesome_set}
|
34
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
35
|
+
s.require_paths = ["lib"]
|
36
|
+
s.rubygems_version = %q{1.3.5}
|
37
|
+
s.summary = %q{A nested set plugin for datamapper}
|
38
|
+
s.test_files = [
|
39
|
+
"spec/dm-is-awesome_set_spec.rb",
|
40
|
+
"spec/spec_helper.rb"
|
41
|
+
]
|
42
|
+
|
43
|
+
if s.respond_to? :specification_version then
|
44
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
45
|
+
s.specification_version = 3
|
46
|
+
|
47
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
48
|
+
s.add_runtime_dependency(%q<dm-core>, ["~> 0.10"])
|
49
|
+
s.add_runtime_dependency(%q<dm-adjust>, ["~> 0.10"])
|
50
|
+
s.add_runtime_dependency(%q<dm-aggregates>, ["~> 0.10"])
|
51
|
+
s.add_development_dependency(%q<rspec>, ["~> 1.2.9"])
|
52
|
+
else
|
53
|
+
s.add_dependency(%q<dm-core>, ["~> 0.10"])
|
54
|
+
s.add_dependency(%q<dm-adjust>, ["~> 0.10"])
|
55
|
+
s.add_dependency(%q<dm-aggregates>, ["~> 0.10"])
|
56
|
+
s.add_dependency(%q<rspec>, ["~> 1.2.9"])
|
57
|
+
end
|
58
|
+
else
|
59
|
+
s.add_dependency(%q<dm-core>, ["~> 0.10"])
|
60
|
+
s.add_dependency(%q<dm-adjust>, ["~> 0.10"])
|
61
|
+
s.add_dependency(%q<dm-aggregates>, ["~> 0.10"])
|
62
|
+
s.add_dependency(%q<rspec>, ["~> 1.2.9"])
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
@@ -0,0 +1,428 @@
|
|
1
|
+
module DataMapper
|
2
|
+
module Is
|
3
|
+
##
|
4
|
+
# Thanks for looking into dm-is-awesome_set. What makes it so awesome? Well,
|
5
|
+
# the fact that it actually works. At least I think it does. Give it a whirl,
|
6
|
+
# and if you come across any bugs let me know (check the readme file for
|
7
|
+
# information). Most of what you will be concerned with is the move method,
|
8
|
+
# though there are some other helper methods for selecting nodes.
|
9
|
+
# The way you use it in your model is like so:
|
10
|
+
#
|
11
|
+
# def ModelName
|
12
|
+
# include DataMapper::Resource
|
13
|
+
# # ... set up your properties ...
|
14
|
+
# is :awesome_set, :scope => [:col1, :col2], :child_key => [:parent_id]
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# Note that scope is optional, and :child_key's default is [:parent_id]
|
18
|
+
|
19
|
+
module AwesomeSet
|
20
|
+
# Available options for is awesome_set:
|
21
|
+
# :scope => array of keys for scope (default is [])
|
22
|
+
# :child_key => array of keys for setting the parent-child relationship (default is [:parent_id])
|
23
|
+
|
24
|
+
def is_awesome_set(options={})
|
25
|
+
extend DataMapper::Is::AwesomeSet::ClassMethods
|
26
|
+
include DataMapper::Is::AwesomeSet::InstanceMethods
|
27
|
+
|
28
|
+
opts = set_options(options)
|
29
|
+
[:child_key, :scope].each {|var| raise "#{var} must be an Array" unless opts[var].is_a?(Array)}
|
30
|
+
|
31
|
+
property :parent_id, Integer, :min => 0, :writer => :protected
|
32
|
+
|
33
|
+
property :lft, Integer, :writer => :private, :index => true
|
34
|
+
property :rgt, Integer, :writer => :private, :index => true
|
35
|
+
|
36
|
+
class_opts = {:model => self.name, :child_key => opts[:child_key], :order => [:lft.asc] }
|
37
|
+
belongs_to :parent, class_opts
|
38
|
+
has n, :children, class_opts
|
39
|
+
|
40
|
+
before :save_self do
|
41
|
+
move_without_saving(:root) if lft.nil? #You don't want to use new_record? here. Trust me, you don't.
|
42
|
+
end
|
43
|
+
|
44
|
+
end # def is_awesome_set
|
45
|
+
|
46
|
+
module ClassMethods
|
47
|
+
def set_options(options) #:nodoc:
|
48
|
+
@ias_options = { :child_key => [:parent_id], :scope => [] }.merge(options)
|
49
|
+
end
|
50
|
+
|
51
|
+
def ias_options; @ias_options || superclass.ias_options end #:nodoc:
|
52
|
+
|
53
|
+
def child_keys; ias_options[:child_key]; end
|
54
|
+
def scope_keys; ias_options[:scope]; end
|
55
|
+
def is_nested_set? #:nodoc:
|
56
|
+
true
|
57
|
+
end
|
58
|
+
|
59
|
+
# Checks to see if the hash or object contains a valid scope by checking attributes or keys
|
60
|
+
def valid_scope?(hash)
|
61
|
+
return true if hash.is_a?(self)
|
62
|
+
return false unless hash.is_a?(Hash)
|
63
|
+
scope_keys.each { |sk| return false unless hash.keys.include?(sk) }
|
64
|
+
true
|
65
|
+
end
|
66
|
+
|
67
|
+
# Raises an error if the scope is not valid
|
68
|
+
def check_scope(hash)
|
69
|
+
raise 'Invalid scope: ' + hash.inspect if !valid_scope?(hash)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Return only the attributes that deal with the scope, will raise an error on invalid scope
|
73
|
+
def extract_scope(hash)
|
74
|
+
check_scope(hash)
|
75
|
+
ret = {}
|
76
|
+
send_to_obj = hash.is_a?(self)
|
77
|
+
scope_keys.each { |sk| ret[sk] = send_to_obj ? hash.attribute_get(sk) : hash[sk] }
|
78
|
+
ret
|
79
|
+
end
|
80
|
+
|
81
|
+
def adjust_gap!(scoped_set, at, adjustment) #:nodoc:
|
82
|
+
scoped_set.all(:rgt.gt => at).adjust!({:rgt => adjustment},true)
|
83
|
+
scoped_set.all(:lft.gt => at).adjust!({:lft => adjustment},true)
|
84
|
+
end
|
85
|
+
|
86
|
+
# Return a hash that gets the roots
|
87
|
+
def root_hash
|
88
|
+
ret = {}
|
89
|
+
child_keys.each { |ck| ret[ck] = nil }
|
90
|
+
ret
|
91
|
+
end
|
92
|
+
|
93
|
+
|
94
|
+
# Get the root with no args if there is no scope
|
95
|
+
# Pass the scope or an object with scope to get the first root
|
96
|
+
def root(scope = {})
|
97
|
+
scope = extract_scope(scope)
|
98
|
+
get_class.first(scope.merge(root_hash.merge(:order => [:lft.asc])))
|
99
|
+
end
|
100
|
+
|
101
|
+
# Same as @root, but gets all roots
|
102
|
+
def roots(scope = {})
|
103
|
+
scope = extract_scope(scope)
|
104
|
+
get_class.all(scope.merge(root_hash.merge(:order => [:lft.asc])))
|
105
|
+
end
|
106
|
+
|
107
|
+
# Gets the full set with scope behavior like @root
|
108
|
+
def full_set(scope = {})
|
109
|
+
scope = extract_scope(scope)
|
110
|
+
get_class.all(scope.merge(:order => [:lft.asc]))
|
111
|
+
end
|
112
|
+
|
113
|
+
# Retrieves all nodes that do not have children.
|
114
|
+
# This needs to be refactored for more of a DM style, if possible.
|
115
|
+
def leaves(scope = {})
|
116
|
+
scope = extract_scope(scope)
|
117
|
+
get_class.all(scope.merge(:order => [:lft.asc], :conditions => ["`rgt` - `lft` = 1"]))
|
118
|
+
end
|
119
|
+
|
120
|
+
# Since DataMapper looks for all records in a table when using discriminators
|
121
|
+
# when using the parent model , we'll look for the earliest ancestor class
|
122
|
+
# that is a nested set.
|
123
|
+
def get_class #:nodoc:
|
124
|
+
klass = self
|
125
|
+
klass = klass.superclass while klass.superclass.respond_to?(:is_nested_set?) && klass.superclass.is_nested_set?
|
126
|
+
klass
|
127
|
+
end
|
128
|
+
end # mod ClassMethods
|
129
|
+
|
130
|
+
module InstanceMethods
|
131
|
+
##
|
132
|
+
# move self / node to a position in the set. position can _only_ be changed through this
|
133
|
+
#
|
134
|
+
# @example [Usage]
|
135
|
+
# * node.move :higher # moves node higher unless it is at the top of parent
|
136
|
+
# * node.move :lower # moves node lower unless it is at the bottom of parent
|
137
|
+
# * node.move :below => other # moves this node below other resource in the set
|
138
|
+
# * node.move :into => other # same as setting a parent-relationship
|
139
|
+
#
|
140
|
+
# @param vector <Symbol, Hash> A symbol, or a key-value pair that describes the requested movement
|
141
|
+
#
|
142
|
+
# @option :higher<Symbol> move node higher
|
143
|
+
# @option :highest<Symbol> move node to the top of the list (within its parent)
|
144
|
+
# @option :lower<Symbol> move node lower
|
145
|
+
# @option :lowest<Symbol> move node to the bottom of the list (within its parent)
|
146
|
+
# @option :indent<Symbol> move node into sibling above
|
147
|
+
# @option :outdent<Symbol> move node out below its current parent
|
148
|
+
# @option :root<Symbol|Hash|Resource> move node to root. If passed an object / hash, it uses the scope of that. Otherwise, it uses currently set scope.
|
149
|
+
# @option :into<Resource> move node into another node
|
150
|
+
# @option :above<Resource> move node above other node
|
151
|
+
# @option :below<Resource> move node below other node
|
152
|
+
# @option :to<Integer> move node to a specific location in the nested set
|
153
|
+
# @see move_without_saving
|
154
|
+
|
155
|
+
def move(vector)
|
156
|
+
transaction do
|
157
|
+
move_without_saving(vector)
|
158
|
+
save!
|
159
|
+
end
|
160
|
+
reload
|
161
|
+
end
|
162
|
+
|
163
|
+
def level
|
164
|
+
ancestors.length
|
165
|
+
end
|
166
|
+
|
167
|
+
# Gets the root of this node
|
168
|
+
def root
|
169
|
+
get_class.first(root_hash.merge(:lft.lt => lft, :rgt.gt => rgt))
|
170
|
+
end
|
171
|
+
|
172
|
+
# Gets all the roots of this node's tree
|
173
|
+
def roots
|
174
|
+
get_class.all(root_hash.merge(:order => [:lft.asc]))
|
175
|
+
end
|
176
|
+
|
177
|
+
# Gets all ancestors of this node
|
178
|
+
def ancestors
|
179
|
+
get_class.all(scope_hash.merge(:lft.lt => lft, :rgt.gt => rgt, :order => [:lft.asc]))
|
180
|
+
end
|
181
|
+
|
182
|
+
# Same as ancestors, but also including this node
|
183
|
+
def self_and_ancestors
|
184
|
+
get_class.all(scope_hash.merge(:lft.lte => lft, :rgt.gte => rgt, :order => [:lft.asc]))
|
185
|
+
end
|
186
|
+
|
187
|
+
# Gets all nodes that share the same parent node, except for this node
|
188
|
+
def siblings
|
189
|
+
get_class.all(scope_and_parent_hash.merge(:order => [:lft.asc], :lft.not => lft))
|
190
|
+
end
|
191
|
+
|
192
|
+
# Same as siblings, but returns this node as well
|
193
|
+
def self_and_siblings
|
194
|
+
get_class.all(scope_and_parent_hash.merge(:order => [:lft.asc]))
|
195
|
+
end
|
196
|
+
|
197
|
+
# Returns next node with same parent, or nil
|
198
|
+
def next_sibling
|
199
|
+
get_class.first(scope_and_parent_hash.merge(:lft.gt => rgt, :order => [:lft.asc]))
|
200
|
+
end
|
201
|
+
|
202
|
+
# Returns previous node with same parent, or nil
|
203
|
+
def previous_sibling
|
204
|
+
get_class.first(scope_and_parent_hash.merge(:rgt.lt => lft, :order => [:rgt.desc]))
|
205
|
+
end
|
206
|
+
|
207
|
+
# Returns the full set within this scope
|
208
|
+
def full_set
|
209
|
+
get_class.all(scope_hash)
|
210
|
+
end
|
211
|
+
|
212
|
+
# Gets all descendents of this node
|
213
|
+
def descendents
|
214
|
+
get_class.all(scope_hash.merge(:rgt.lt => rgt, :lft.gt => lft, :order => [:lft.asc]))
|
215
|
+
end
|
216
|
+
|
217
|
+
|
218
|
+
# Same as descendents, but returns self as well
|
219
|
+
def self_and_descendents
|
220
|
+
get_class.all(scope_hash.merge(:rgt.lte => rgt, :lft.gte => lft, :order => [:lft.asc]))
|
221
|
+
end
|
222
|
+
|
223
|
+
|
224
|
+
# Fixed spelling for when English majors are peering over your shoulder
|
225
|
+
def descendants; descendents; end
|
226
|
+
def self_and_descendants; self_and_descendents; end
|
227
|
+
|
228
|
+
# Retrieves the nodes without any children.
|
229
|
+
def leaves
|
230
|
+
get_class.leaves(self)
|
231
|
+
end
|
232
|
+
|
233
|
+
def attributes_set(hash) #:nodoc:
|
234
|
+
hash = hash || {}
|
235
|
+
hash.each { |k,v| attribute_set(k,v) }
|
236
|
+
end
|
237
|
+
|
238
|
+
# Destroys the current node and all children nodes, running their before and after hooks
|
239
|
+
# Returns the destroyed objects
|
240
|
+
def destroy
|
241
|
+
sads = self_and_descendants
|
242
|
+
hooks = get_class.const_get('INSTANCE_HOOKS')
|
243
|
+
before_methods = hooks[:destroy][:before].map { |hash| hash[:name] }
|
244
|
+
after_methods = hooks[:destroy][:after].map { |hash| hash[:name] }
|
245
|
+
# Trigger all the before :destroy methods
|
246
|
+
sads.each { |sad| before_methods.each { |bf| sad.send(bf) } }
|
247
|
+
# dup is called here because destroy! likes to clear out the array, understandably.
|
248
|
+
transaction do
|
249
|
+
sads.dup.destroy!
|
250
|
+
adjust_gap!(full_set, lft, -(rgt - lft + 1))
|
251
|
+
end
|
252
|
+
# Now go through after all the after :destroy methods.
|
253
|
+
sads.each { |sad| after_methods.each { |bf| sad.send(bf) } }
|
254
|
+
end
|
255
|
+
|
256
|
+
# Same as @destroy, but does not run the hooks
|
257
|
+
def destroy!
|
258
|
+
sad = self_and_descendants
|
259
|
+
transaction do
|
260
|
+
sad.dup.destroy!
|
261
|
+
adjust_gap!(full_set, lft, -(rgt - lft + 1))
|
262
|
+
end
|
263
|
+
sad
|
264
|
+
end
|
265
|
+
|
266
|
+
protected
|
267
|
+
def skip_adjust=(var) #:nodoc:
|
268
|
+
@skip_adjust = true
|
269
|
+
end
|
270
|
+
|
271
|
+
def adjust_gap!(*args) #:nodoc:
|
272
|
+
get_class.adjust_gap!(*args)
|
273
|
+
end
|
274
|
+
|
275
|
+
def get_finder_hash(*args)
|
276
|
+
ret = {}
|
277
|
+
args.each { |arg| get_class.ias_options[arg].each { |s| ret[s] = send(s) } }
|
278
|
+
ret
|
279
|
+
end
|
280
|
+
|
281
|
+
def root_hash
|
282
|
+
ret = {}
|
283
|
+
get_class.child_keys.each { |ck| ret[ck] = nil }
|
284
|
+
scope_hash.merge(ret)
|
285
|
+
end
|
286
|
+
|
287
|
+
def scope_and_parent_hash
|
288
|
+
get_finder_hash(:child_key, :scope)
|
289
|
+
end
|
290
|
+
|
291
|
+
def extract_scope(hash)
|
292
|
+
get_class.extract_scope(hash)
|
293
|
+
end
|
294
|
+
|
295
|
+
def scope_hash
|
296
|
+
get_finder_hash(:scope)
|
297
|
+
end
|
298
|
+
|
299
|
+
def parent_hash
|
300
|
+
get_finder_hash(:child_key)
|
301
|
+
end
|
302
|
+
|
303
|
+
def same_scope?(obj)
|
304
|
+
case obj
|
305
|
+
when get_class then scope_hash == obj.send(:scope_hash)
|
306
|
+
when Hash then scope_hash == obj
|
307
|
+
when nil then true
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
def valid_scope?(hash)
|
312
|
+
get_class.valid_scope?(hash)
|
313
|
+
end
|
314
|
+
|
315
|
+
def move_without_saving(vector)
|
316
|
+
# Do some checking of the variable...
|
317
|
+
if vector.respond_to?(:'[]') && vector.respond_to?(:size) && vector.size == 1
|
318
|
+
action = vector.keys[0]
|
319
|
+
obj = vector[action]
|
320
|
+
elsif vector.is_a?(Symbol)
|
321
|
+
obj = nil
|
322
|
+
action = vector
|
323
|
+
else
|
324
|
+
raise 'You must pass either a symbol or a hash with one property to the method "move".'
|
325
|
+
end
|
326
|
+
|
327
|
+
|
328
|
+
# Convenience methods
|
329
|
+
ret_value = case action
|
330
|
+
when :higher then previous_sibling ? move_without_saving(:above => previous_sibling) : false
|
331
|
+
when :highest then move_without_saving(:to => parent ? (parent.lft + 1) : 1)
|
332
|
+
when :lower then next_sibling ? move_without_saving(:below => next_sibling) : false
|
333
|
+
when :lowest then parent ? move_without_saving(:to => parent.rgt - 1) : move_without_saving(:root)
|
334
|
+
when :indent then previous_sibling ? move_without_saving(:into => previous_sibling) : false
|
335
|
+
when :outdent then parent ? move_without_saving(:below => parent) : false
|
336
|
+
else :no_action
|
337
|
+
end
|
338
|
+
return ret_value unless ret_value == :no_action
|
339
|
+
|
340
|
+
this_gap = lft.to_i > 0 && rgt.to_i > 0 ? rgt - lft : 1
|
341
|
+
old_parent = parent
|
342
|
+
new_scope = nil
|
343
|
+
max = nil
|
344
|
+
|
345
|
+
# Here's where the real heavy lifting happens. Any action can be taken
|
346
|
+
# care of by :root, :above, :below, or :to
|
347
|
+
pos, adjust_at, p_obj = case action
|
348
|
+
when :root
|
349
|
+
new_scope = obj ? extract_scope(obj) : scope_hash
|
350
|
+
max = (get_class.max(:rgt, new_scope) || 0) + 1
|
351
|
+
when :into then [obj.rgt, obj.rgt - 1, obj]
|
352
|
+
when :above then [obj.lft, obj.lft - 1, obj.parent]
|
353
|
+
when :below then [obj.rgt + 1, obj.rgt, obj.parent]
|
354
|
+
when :to
|
355
|
+
pos = obj.to_i
|
356
|
+
p_obj = get_class.first(scope_hash.merge(:lft.lt => pos, :rgt.gt => pos, :order => [:lft.desc]))
|
357
|
+
[pos, pos - 1, p_obj]
|
358
|
+
else raise 'Invalid action sent to the method "move": ' + action.to_s
|
359
|
+
end
|
360
|
+
|
361
|
+
old_scope = nil
|
362
|
+
new_scope ||= extract_scope(p_obj) if p_obj
|
363
|
+
|
364
|
+
max ||= (get_class.max(:rgt, new_scope || scope_hash) || 0) + 1
|
365
|
+
if pos == 0 || pos > max
|
366
|
+
raise "You cannot move a node outside of the bounds of the tree. You passed: #{pos}. Acceptable numbers are 1 through #{max}"
|
367
|
+
end
|
368
|
+
|
369
|
+
raise 'You are trying to move a node into one that has not been saved yet.' if p_obj && p_obj.lft.nil?
|
370
|
+
|
371
|
+
if lft
|
372
|
+
adjustment = pos < lft ? this_gap + 1 : 0
|
373
|
+
raise 'Illegal move: you are trying to move a node within itself' if pos.between?(lft+adjustment,rgt+adjustment) && same_scope?(new_scope)
|
374
|
+
end
|
375
|
+
|
376
|
+
# make a new hole and assign parent
|
377
|
+
#
|
378
|
+
# Note: with identity map on an already saved object, making a hole
|
379
|
+
# will alter lft & rgt values immediately, so we need to keep copies
|
380
|
+
old_lft, old_rgt = lft, rgt if lft && rgt
|
381
|
+
adjust_gap!(get_class.full_set(new_scope || scope_hash) , adjust_at, this_gap + 1) if adjust_at
|
382
|
+
|
383
|
+
# Do we need to move the node (already present in the tree), or just save the attributes?
|
384
|
+
if lft && (pos != old_lft || !same_scope?(new_scope))
|
385
|
+
# Move elements
|
386
|
+
if same_scope?(new_scope)
|
387
|
+
move_by = pos - (old_lft + adjustment)
|
388
|
+
full_set.all(:lft.gte => old_lft + adjustment, :rgt.lte => old_rgt + adjustment).adjust!({:lft => move_by, :rgt => move_by}, true)
|
389
|
+
else # Things have to be done a little differently if moving scope
|
390
|
+
move_by = pos - old_lft
|
391
|
+
old_scope = extract_scope(self)
|
392
|
+
sads = self_and_descendants
|
393
|
+
sads.adjust!({:lft => move_by, :rgt => move_by}, true)
|
394
|
+
# Update the attributes to match how they are in the database now.
|
395
|
+
# Be sure to do this between adjust! and setting the new scope
|
396
|
+
attribute_set(:rgt, old_rgt + move_by)
|
397
|
+
attribute_set(:lft, old_lft + move_by)
|
398
|
+
|
399
|
+
sads.each { |d| d.update!(new_scope)}
|
400
|
+
end
|
401
|
+
|
402
|
+
# Close hole
|
403
|
+
if old_scope
|
404
|
+
adjust_gap!(get_class.full_set(old_scope), old_lft, -(this_gap + 1))
|
405
|
+
else
|
406
|
+
adjustment += 1 if parent == old_parent
|
407
|
+
adjust_gap!(full_set, old_lft + adjustment, -(this_gap + 1))
|
408
|
+
end
|
409
|
+
else # just save the attributes
|
410
|
+
attribute_set(:lft, pos)
|
411
|
+
attribute_set(:rgt, lft + this_gap)
|
412
|
+
attributes_set(p_obj.send(:scope_hash)) if p_obj
|
413
|
+
end
|
414
|
+
# We set parent here because we don't want to throw errors with dirty
|
415
|
+
# tracking during all of the adjust! and update!(scope) calls
|
416
|
+
self.parent = p_obj
|
417
|
+
|
418
|
+
end
|
419
|
+
|
420
|
+
def get_class #:no_doc:
|
421
|
+
self.class.get_class
|
422
|
+
end
|
423
|
+
end # mod InstanceMethods
|
424
|
+
|
425
|
+
Model.send(:include, self)
|
426
|
+
end # mod AwesomeSet
|
427
|
+
end # mod Is
|
428
|
+
end # mod DM
|