dm-is-awesome_set 0.10.2 → 0.11.0

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 CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2008 Jeremy Nicoll (http://gnexp.com, jnicoll@gnexp.com)
1
+ Copyright (c) 2008,2009,2010 Jeremy Nicoll (http://gnexp.com, jnicoll@gnexp.com)
2
2
 
3
3
  Some code and documentation janked from dm-is-nested_set by Sindre Aarsaether
4
4
  (somebee.com)
data/Rakefile CHANGED
@@ -6,6 +6,8 @@ begin
6
6
  gem 'jeweler', '>= 1.4'
7
7
  require 'jeweler'
8
8
 
9
+ FileList['tasks/**/*.rake'].each { |task| load task }
10
+
9
11
  Jeweler::Tasks.new do |gem|
10
12
 
11
13
  gem.name = "dm-is-awesome_set"
@@ -15,9 +17,9 @@ begin
15
17
  gem.homepage = "http://github.com/snusnu/dm-is-awesome_set"
16
18
  gem.authors = ["Jeremy Nicoll", "David Haslem", "Martin Gamsjaeger (snusnu)"]
17
19
 
18
- gem.add_dependency 'dm-core', '~> 0.10'
19
- gem.add_dependency 'dm-adjust', '~> 0.10'
20
- gem.add_dependency 'dm-aggregates', '~> 0.10'
20
+ gem.add_dependency 'dm-core', '~> 0.10'
21
+ gem.add_dependency 'dm-adjust', '~> 0.10'
22
+ gem.add_dependency 'dm-aggregates', '~> 0.10'
21
23
 
22
24
  gem.add_development_dependency 'rspec', '~> 1.2.9'
23
25
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.10.2
1
+ 0.11.0
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{dm-is-awesome_set}
8
- s.version = "0.10.2"
8
+ s.version = "0.11.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Jeremy Nicoll", "David Haslem", "Martin Gamsjaeger (snusnu)"]
12
- s.date = %q{2010-02-19}
12
+ s.date = %q{2010-02-17}
13
13
  s.description = %q{A library that lets any datamapper model act like a nested set}
14
14
  s.email = %q{jnicoll@gnexp.com}
15
15
  s.extra_rdoc_files = [
@@ -26,9 +26,16 @@ Gem::Specification.new do |s|
26
26
  "VERSION",
27
27
  "dm-is-awesome_set.gemspec",
28
28
  "lib/dm-is-awesome_set.rb",
29
- "lib/dm-is-awesome_set/is/awesome_set.rb",
30
29
  "spec/dm-is-awesome_set_spec.rb",
31
- "spec/spec_helper.rb"
30
+ "spec/rcov.opts",
31
+ "spec/spec.opts",
32
+ "spec/spec_helper.rb",
33
+ "tasks/changelog.rake",
34
+ "tasks/ci.rake",
35
+ "tasks/metrics.rake",
36
+ "tasks/spec.rake",
37
+ "tasks/yard.rake",
38
+ "tasks/yardstick.rake"
32
39
  ]
33
40
  s.homepage = %q{http://github.com/snusnu/dm-is-awesome_set}
34
41
  s.rdoc_options = ["--charset=UTF-8"]
@@ -1,5 +1,428 @@
1
- require 'pathname'
2
- ['dm-core', 'dm-adjust', 'dm-aggregates', 'dm-validations'].each do |dm_var|
3
- require dm_var
4
- end
5
- require Pathname(__FILE__).dirname.expand_path / 'dm-is-awesome_set' / 'is' / 'awesome_set.rb'
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 : scope_hash == obj.send(:scope_hash)
306
+ when Hash : scope_hash == obj
307
+ when nil : 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 : previous_sibling ? move_without_saving(:above => previous_sibling) : false
331
+ when :highest : move_without_saving(:to => parent ? (parent.lft + 1) : 1)
332
+ when :lower : next_sibling ? move_without_saving(:below => next_sibling) : false
333
+ when :lowest : parent ? move_without_saving(:to => parent.rgt - 1) : move_without_saving(:root)
334
+ when :indent : previous_sibling ? move_without_saving(:into => previous_sibling) : false
335
+ when :outdent : 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 : [obj.rgt, obj.rgt - 1, obj]
352
+ when :above : [obj.lft, obj.lft - 1, obj.parent]
353
+ when :below : [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
@@ -1,4 +1,4 @@
1
- require File.dirname(__FILE__) + '/spec_helper'
1
+ require 'spec_helper'
2
2
 
3
3
 
4
4
  scope = {:scope => 1, :scope_2 => 2}
@@ -0,0 +1,6 @@
1
+ --exclude "spec"
2
+ --sort coverage
3
+ --callsites
4
+ --xrefs
5
+ --profile
6
+ --text-summary
@@ -0,0 +1,4 @@
1
+ --colour
2
+ --loadby random
3
+ --format specdoc
4
+ --backtrace
@@ -1,12 +1,15 @@
1
1
  require 'rubygems'
2
- $:.push File.join(File.dirname(__FILE__), '..', 'lib')
3
- require 'dm-is-awesome_set'
4
2
 
5
- # Needed for the discriminator
6
- gem 'dm-types', '>=0.9.7'
3
+ require 'dm-core'
4
+ require 'dm-adjust'
5
+ require 'dm-aggregates'
7
6
  require 'dm-types'
8
7
  require 'dm-validations'
9
8
 
9
+ $LOAD_PATH.unshift File.expand_path('../lib', __FILE__)
10
+ require 'dm-is-awesome_set'
11
+
12
+
10
13
  ENV["SQLITE3_SPEC_URI"] ||= 'sqlite3::memory:'
11
14
  ENV["MYSQL_SPEC_URI"] ||= 'mysql://localhost/dm-is_awesome_set_test'
12
15
  ENV["POSTGRES_SPEC_URI"] ||= 'postgres://postgres@localhost/dm-is_awesome_set_test'
@@ -0,0 +1,20 @@
1
+ require 'time'
2
+
3
+ desc 'update changelog'
4
+ task :changelog do
5
+ File.open('CHANGELOG', 'w+') do |changelog|
6
+ `git log -z --abbrev-commit`.split("\0").each do |commit|
7
+ next if commit =~ /^Merge: \d*/
8
+ ref, author, time, _, title, _, message = commit.split("\n", 7)
9
+ ref = ref[/commit ([0-9a-f]+)/, 1]
10
+ author = author[/Author: (.*)/, 1].strip
11
+ time = Time.parse(time[/Date: (.*)/, 1]).utc
12
+ title.strip!
13
+
14
+ changelog.puts "[#{ref} | #{time}] #{author}"
15
+ changelog.puts '', " * #{title}"
16
+ changelog.puts '', message.rstrip if message
17
+ changelog.puts
18
+ end
19
+ end
20
+ end
@@ -0,0 +1 @@
1
+ task :ci => [ :verify_measurements, 'metrics:all' ]
@@ -0,0 +1,36 @@
1
+ begin
2
+ require 'metric_fu'
3
+ rescue LoadError
4
+ namespace :metrics do
5
+ task :all do
6
+ abort 'metric_fu is not available. In order to run metrics:all, you must: gem install metric_fu'
7
+ end
8
+ end
9
+ end
10
+
11
+ begin
12
+ require 'reek/adapters/rake_task'
13
+
14
+ Reek::RakeTask.new do |t|
15
+ t.fail_on_error = true
16
+ t.verbose = false
17
+ t.source_files = 'lib/**/*.rb'
18
+ end
19
+ rescue LoadError
20
+ task :reek do
21
+ abort 'Reek is not available. In order to run reek, you must: gem install reek'
22
+ end
23
+ end
24
+
25
+ begin
26
+ require 'roodi'
27
+ require 'roodi_task'
28
+
29
+ RoodiTask.new do |t|
30
+ t.verbose = false
31
+ end
32
+ rescue LoadError
33
+ task :roodi do
34
+ abort 'Roodi is not available. In order to run roodi, you must: gem install roodi'
35
+ end
36
+ end
@@ -0,0 +1,25 @@
1
+ require 'spec/rake/spectask'
2
+ require 'spec/rake/verify_rcov'
3
+
4
+ spec_defaults = lambda do |spec|
5
+ spec.pattern = 'spec/**/*_spec.rb'
6
+ spec.libs << 'lib' << 'spec'
7
+ spec.spec_opts << '--options' << 'spec/spec.opts'
8
+ end
9
+
10
+ Spec::Rake::SpecTask.new(:spec, &spec_defaults)
11
+
12
+ Spec::Rake::SpecTask.new(:rcov) do |rcov|
13
+ spec_defaults.call(rcov)
14
+ rcov.rcov = true
15
+ rcov.rcov_opts = File.read('spec/rcov.opts').split(/\s+/)
16
+ end
17
+
18
+ RCov::VerifyTask.new(:verify_rcov => :rcov) do |rcov|
19
+ rcov.threshold = 100
20
+ end
21
+
22
+ task :spec => :check_dependencies
23
+ task :rcov => :check_dependencies
24
+
25
+ task :default => :spec
@@ -0,0 +1,9 @@
1
+ begin
2
+ require 'yard'
3
+
4
+ YARD::Rake::YardocTask.new
5
+ rescue LoadError
6
+ task :yard do
7
+ abort 'YARD is not available. In order to run yard, you must: gem install yard'
8
+ end
9
+ end
@@ -0,0 +1,19 @@
1
+ begin
2
+ require 'pathname'
3
+ require 'yardstick/rake/measurement'
4
+ require 'yardstick/rake/verify'
5
+
6
+ # yardstick_measure task
7
+ Yardstick::Rake::Measurement.new
8
+
9
+ # verify_measurements task
10
+ Yardstick::Rake::Verify.new do |verify|
11
+ verify.threshold = 100
12
+ end
13
+ rescue LoadError
14
+ %w[ yardstick_measure verify_measurements ].each do |name|
15
+ task name.to_s do
16
+ abort "Yardstick is not available. In order to run #{name}, you must: gem install yardstick"
17
+ end
18
+ end
19
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dm-is-awesome_set
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.2
4
+ version: 0.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeremy Nicoll
@@ -11,7 +11,7 @@ autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
13
 
14
- date: 2010-02-19 00:00:00 -05:00
14
+ date: 2010-02-17 00:00:00 -05:00
15
15
  default_executable:
16
16
  dependencies:
17
17
  - !ruby/object:Gem::Dependency
@@ -73,9 +73,16 @@ files:
73
73
  - VERSION
74
74
  - dm-is-awesome_set.gemspec
75
75
  - lib/dm-is-awesome_set.rb
76
- - lib/dm-is-awesome_set/is/awesome_set.rb
77
76
  - spec/dm-is-awesome_set_spec.rb
77
+ - spec/rcov.opts
78
+ - spec/spec.opts
78
79
  - spec/spec_helper.rb
80
+ - tasks/changelog.rake
81
+ - tasks/ci.rake
82
+ - tasks/metrics.rake
83
+ - tasks/spec.rake
84
+ - tasks/yard.rake
85
+ - tasks/yardstick.rake
79
86
  has_rdoc: true
80
87
  homepage: http://github.com/snusnu/dm-is-awesome_set
81
88
  licenses: []
@@ -1,428 +0,0 @@
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