dm-is-awesome_set 0.10.2 → 0.11.0

Sign up to get free protection for your applications and to get access to all the features.
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