acts_as_nested_interval 0.2.0 → 0.3.0.pre

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +11 -4
  3. data/lib/acts_as_nested_interval.rb +3 -30
  4. data/lib/acts_as_nested_interval/associations.rb +41 -0
  5. data/lib/acts_as_nested_interval/callbacks.rb +18 -8
  6. data/lib/acts_as_nested_interval/configuration.rb +9 -2
  7. data/lib/acts_as_nested_interval/core_ext/rational.rb +7 -0
  8. data/lib/acts_as_nested_interval/instance_methods.rb +49 -106
  9. data/lib/acts_as_nested_interval/version.rb +1 -1
  10. data/test/acts_as_nested_interval_test.rb +64 -160
  11. metadata +35 -87
  12. data/test/dummy/README.rdoc +0 -28
  13. data/test/dummy/Rakefile +0 -6
  14. data/test/dummy/app/assets/javascripts/application.js +0 -13
  15. data/test/dummy/app/assets/stylesheets/application.css +0 -15
  16. data/test/dummy/app/controllers/application_controller.rb +0 -5
  17. data/test/dummy/app/helpers/application_helper.rb +0 -2
  18. data/test/dummy/app/models/region.rb +0 -5
  19. data/test/dummy/app/views/layouts/application.html.erb +0 -14
  20. data/test/dummy/bin/bundle +0 -3
  21. data/test/dummy/bin/rails +0 -4
  22. data/test/dummy/bin/rake +0 -4
  23. data/test/dummy/config.ru +0 -4
  24. data/test/dummy/config/application.rb +0 -23
  25. data/test/dummy/config/boot.rb +0 -5
  26. data/test/dummy/config/database.yml +0 -31
  27. data/test/dummy/config/environment.rb +0 -5
  28. data/test/dummy/config/environments/development.rb +0 -37
  29. data/test/dummy/config/environments/production.rb +0 -83
  30. data/test/dummy/config/environments/test.rb +0 -39
  31. data/test/dummy/config/initializers/backtrace_silencers.rb +0 -7
  32. data/test/dummy/config/initializers/cookies_serializer.rb +0 -3
  33. data/test/dummy/config/initializers/filter_parameter_logging.rb +0 -4
  34. data/test/dummy/config/initializers/inflections.rb +0 -16
  35. data/test/dummy/config/initializers/mime_types.rb +0 -4
  36. data/test/dummy/config/initializers/session_store.rb +0 -3
  37. data/test/dummy/config/initializers/wrap_parameters.rb +0 -14
  38. data/test/dummy/config/locales/en.yml +0 -23
  39. data/test/dummy/config/routes.rb +0 -56
  40. data/test/dummy/config/secrets.yml +0 -22
  41. data/test/dummy/db/migrate/20120302143528_create_regions.rb +0 -15
  42. data/test/dummy/db/migrate/20121004204252_change_interval_precision.rb +0 -6
  43. data/test/dummy/db/schema.rb +0 -31
  44. data/test/dummy/log/development.log +0 -140
  45. data/test/dummy/log/test.log +0 -7048
  46. data/test/dummy/public/404.html +0 -67
  47. data/test/dummy/public/422.html +0 -67
  48. data/test/dummy/public/500.html +0 -66
  49. data/test/dummy/public/favicon.ico +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 252c9518f088df6b078b315b97ae732f1e5fffc7
4
- data.tar.gz: a16f8ba25772672b1794742466883ac7d61e62b0
3
+ metadata.gz: 727dd5393b81d22ca456a5ac3f2661287f909a42
4
+ data.tar.gz: ff0dc310818337c838c1ac91c9f6a9e7054bc282
5
5
  SHA512:
6
- metadata.gz: 60417ca3dffb7310836bd95d641c6a78dc1cf49668af22d2066eab081767b6cf70544a43f1a3948354a6fbbb001c31bc4e6bc52041bc52e4687645936e6402ec
7
- data.tar.gz: 249f1d4171f569687190f46412438b13b754c418d82d23de1e9ee6f4a4334e78de892dc7d35fe9a7dc8c20cdc1b206be578bd2dc16857729d0e173e51a89d615
6
+ metadata.gz: fadcf0336a54a3d6deb0ae23d5a11c96c1e229a39653dca8568a1f9bf0b174057e7cb422751517409a1635857690d1a89925d959986896eeb68fcdb57b2fe39b
7
+ data.tar.gz: b33657f0b7cc2458a00827bb7b0b7fd24d39e8217bec60253acbbce2ff2487d572e0fe0669ee091d30bf7c09460bae0c7e6385b52b005af4a96d3016781707c2
data/README.md CHANGED
@@ -17,8 +17,10 @@ If your database supports recursive queryes (`WITH RECURSIVE`) or specific custo
17
17
  ## Install
18
18
 
19
19
  ```ruby
20
- # add to Gemfile
21
- gem 'acts_as_nested_interval'
20
+ # add to Gemfile when you use Ruby > 2.0 or Rails >= 4.0
21
+ gem 'acts_as_nested_interval', '~> 0.2.0'
22
+ # Orherwise
23
+ gem 'acts_as_nested_interval', '~> 0.1.1' # This version could have less features than actual version, check legacy branch.
22
24
  ```
23
25
 
24
26
  ```sh
@@ -49,6 +51,7 @@ add_index :regions, :lftp
49
51
  add_index :regions, :lftq
50
52
  add_index :regions, :lft
51
53
  add_index :regions, :rgt
54
+ add_index :regions, [:lftp, :lftq, :rgtq, :rgtp], unique: true
52
55
  ```
53
56
 
54
57
  ## Usage
@@ -59,8 +62,11 @@ point data types in the database.
59
62
  This act provides these named scopes:
60
63
 
61
64
  ```ruby
62
- Region.roots # returns roots of tree.
63
- Region.preorder # returns records for preorder traversal.
65
+ Region.roots # returns roots of tree.
66
+ Region.preorder # returns records for preorder traversal.
67
+ Region.ancestors_of(node) # returns all ancestors of given node
68
+ Region.descendants_of(node) # returns all descendants of given node
69
+ Region.siblings_of(node) # returns all siblings of given node
64
70
  ```
65
71
 
66
72
  This act provides these instance methods:
@@ -70,6 +76,7 @@ Region.parent # returns parent of record.
70
76
  Region.children # returns children of record.
71
77
  Region.ancestors # returns scoped ancestors of record.
72
78
  Region.descendants # returns scoped descendants of record.
79
+ Region.siblings # returns scoped siblings of record.
73
80
  Region.depth # returns depth of record.
74
81
  ```
75
82
 
@@ -5,12 +5,14 @@
5
5
  # https://github.com/clyfe
6
6
 
7
7
  require 'acts_as_nested_interval/core_ext/integer'
8
+ require 'acts_as_nested_interval/core_ext/rational'
8
9
  require 'acts_as_nested_interval/version'
9
10
  require 'acts_as_nested_interval/constants'
10
11
  require 'acts_as_nested_interval/configuration'
11
12
  require 'acts_as_nested_interval/callbacks'
12
13
  require 'acts_as_nested_interval/instance_methods'
13
14
  require 'acts_as_nested_interval/class_methods'
15
+ require 'acts_as_nested_interval/associations'
14
16
 
15
17
  # This act implements a nested-interval tree. You can find all descendants
16
18
  # or all ancestors with just one select query. You can insert and delete
@@ -28,46 +30,17 @@ module ActsAsNestedInterval
28
30
  # * <tt>:dependent</tt> -- dependency between the parent node and children nodes (default :restrict)
29
31
  def acts_as_nested_interval(options = {})
30
32
  # Refactored
31
- # TODO: table_exists?
32
33
  cattr_accessor :nested_interval
33
34
 
34
35
  self.nested_interval = Configuration.new( self, **options )
35
36
 
36
- if nested_interval.fraction_cache?
37
- scope :preorder, -> { order(rgt: :desc, lftp: :asc) }
38
- else
39
- scope :preorder, -> { order('1.0 * rgtp / rgtq DESC, lftp ASC') }
40
- end
41
- # When?
42
- #scope :preorder, -> { order('nested_interval_rgt(lftp, lftq) DESC, lftp ASC') }
43
-
44
-
45
- # OLD CODE
46
- #cattr_accessor :nested_interval_foreign_key
47
- #cattr_accessor :nested_interval_scope_columns
48
- cattr_accessor :nested_interval_lft_index
49
- #cattr_accessor :nested_interval_dependent
50
-
51
- cattr_accessor :virtual_root
52
- self.virtual_root = !!options[:virtual_root]
53
-
54
- #self.nested_interval_foreign_key = options[:foreign_key] || :parent_id
55
- #self.nested_interval_scope_columns = Array(options[:scope_columns])
56
- self.nested_interval_lft_index = options[:lft_index]
57
- #self.nested_interval_dependent = options[:dependent] || :restrict_with_exception
58
-
59
- belongs_to :parent, class_name: name, foreign_key: nested_interval.foreign_key
60
- has_many :children, class_name: name, foreign_key: nested_interval.foreign_key,
61
- dependent: nested_interval.dependent
62
- scope :roots, -> { where(nested_interval.foreign_key => nil) }
63
-
64
37
  if self.table_exists? # Fix problem with migrating without table
65
38
  include ActsAsNestedInterval::InstanceMethods
66
39
  include ActsAsNestedInterval::Callbacks
40
+ include ActsAsNestedInterval::Associations
67
41
  extend ActsAsNestedInterval::ClassMethods
68
42
  end
69
43
  end
70
44
  end
71
45
  end
72
46
 
73
- #ActiveRecord::Base.send :extend, ActsAsNestedInterval
@@ -0,0 +1,41 @@
1
+ module ActsAsNestedInterval
2
+ module Associations
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ belongs_to :parent, class_name: name, foreign_key: nested_interval.foreign_key
7
+ has_many :children, class_name: name, foreign_key: nested_interval.foreign_key,
8
+ dependent: nested_interval.dependent
9
+ scope :roots, -> { where(nested_interval.foreign_key => nil) }
10
+
11
+ scope :ancestors_of, ->(node){ where("rgt >= CAST(:rgt AS FLOAT) AND lft < CAST(:lft AS FLOAT)", rgt: node.rgt, lft: node.lft) }
12
+ scope :subtree_of, ->(node){ where( "lft BETWEEN :lft AND :rgt", rgt: node.rgt, lft: node.lft ) } # Simple version
13
+ scope :descendants_of, ->(node){ subtree_of(node).where.not(id: node.id) }
14
+ scope :siblings_of, ->(node){ fkey = nested_interval.foreign_key; where( fkey => node.send(fkey) ).where.not(id: node.id) }
15
+
16
+ if nested_interval.fraction_cache?
17
+ scope :preorder, -> { order(rgt: :desc, lftp: :asc) }
18
+ else
19
+ scope :preorder, -> { order('1.0 * rgtp / rgtq DESC, lftp ASC') }
20
+ end
21
+ end
22
+
23
+ def ancestor_of?(node)
24
+ left < node.left && right >= node.right
25
+ end
26
+
27
+ def ancestors
28
+ nested_interval_scope.ancestors_of(self)
29
+ end
30
+
31
+ def descendants
32
+ nested_interval_scope.descendants_of(self)
33
+ end
34
+
35
+ def siblings
36
+ nested_interval_scope.siblings_of(self)
37
+ end
38
+
39
+
40
+ end
41
+ end
@@ -17,26 +17,36 @@ module ActsAsNestedInterval
17
17
 
18
18
  # Creates record.
19
19
  def create_nested_interval
20
- if read_attribute(nested_interval.foreign_key).nil?
21
- set_nested_interval_for_top
22
- else
23
- set_nested_interval *parent.lock!.next_child_lft
24
- end
20
+ set_nested_interval(parent.present? ? parent.next_child_lft : next_root_lft )
25
21
  end
26
22
 
27
23
  # Updates record, updating descendants if parent association updated,
28
24
  # in which case caller should first acquire table lock.
29
25
  def update_nested_interval
30
- changed = send(:"#{nested_interval.foreign_key}_changed?")
31
- if !changed
26
+ return if moving?
27
+ unless node_moved?
32
28
  db_self = self.class.find(id).lock!
33
29
  write_attribute(nested_interval.foreign_key, db_self.read_attribute(nested_interval.foreign_key))
34
- set_nested_interval db_self.lftp, db_self.lftq
30
+ set_nested_interval Rational(db_self.lftp, db_self.lftq)
35
31
  else
36
32
  # No locking in this case -- caller should have acquired table lock.
37
33
  update_nested_interval_move
38
34
  end
39
35
  end
36
+
37
+ def move!
38
+ return if moving?
39
+ self.class.nested_interval.moving = true
40
+ transaction do
41
+ yield
42
+ end
43
+ ensure
44
+ self.class.nested_interval.moving = false
45
+ end
46
+
47
+ def moving?
48
+ !!self.class.nested_interval.moving
49
+ end
40
50
 
41
51
  end
42
52
  end
@@ -2,13 +2,16 @@ module ActsAsNestedInterval
2
2
  class Configuration
3
3
 
4
4
  attr_reader :foreign_key, :dependent, :scope_columns
5
+ attr_accessor :moving
5
6
 
6
7
  # multiple_roots - allow more than one root
7
- def initialize( model, virtual_root: false, foreign_key: :parent_id, dependent: :restrict_with_exception, scope_columns: [] )
8
+ def initialize( model, virtual_root: false, foreign_key: :parent_id, dependent: :restrict_with_exception, scope_columns: [], moveable: true)
8
9
  @multiple_roots = !!virtual_root
9
- @foreign_key = foreign_key
10
+ @foreign_key = foreign_key.to_sym
10
11
  @dependent = dependent
11
12
  @scope_columns = *scope_columns
13
+ @moveable = moveable && !model.readonly_attributes.include?(foreign_key) # Fix issue #9
14
+ @moving = false
12
15
 
13
16
  check_model_columns( model )
14
17
  end
@@ -21,6 +24,10 @@ module ActsAsNestedInterval
21
24
  @fraction_cache
22
25
  end
23
26
 
27
+ def moveable?
28
+ @moveable
29
+ end
30
+
24
31
  private
25
32
 
26
33
  def check_model_columns( model )
@@ -0,0 +1,7 @@
1
+ module Mediant
2
+ refine Rational do
3
+ def mediant(a)
4
+ Rational( numerator + a.numerator, denominator + a.denominator )
5
+ end
6
+ end
7
+ end
@@ -1,50 +1,23 @@
1
+ using Mediant
2
+
1
3
  module ActsAsNestedInterval
2
4
  module InstanceMethods
5
+
3
6
  extend ActiveSupport::Concern
4
7
 
5
8
  # selectively define #descendants according to table features
6
9
  included do
7
-
8
- if nested_interval.fraction_cache?
9
-
10
- def descendants
11
- nested_interval_scope.where( "lftp > :lftp AND lft BETWEEN :lft AND :rgt", lftp: lftp, rgt: rgt, lft: lft )
12
- end
13
-
14
- else
15
-
16
- def descendants
17
- quoted_table_name = self.class.quoted_table_name
18
- nested_interval_scope.where <<-SQL
19
- ( #{quoted_table_name}.lftp != #{rgtp} OR
20
- #{quoted_table_name}.lftq != #{rgtq}
21
- ) AND
22
- #{quoted_table_name}.lftp BETWEEN
23
- 1 + #{quoted_table_name}.lftq * CAST(#{lftp} AS BIGINT) / #{lftq} AND
24
- #{quoted_table_name}.lftq * CAST(#{rgtp} AS BIGINT) / #{rgtq}
25
- SQL
26
- end
27
-
28
- end
29
-
10
+ validate :disallow_circular_dependency
30
11
  end
31
12
 
32
- def set_nested_interval(lftp, lftq)
33
- self.lftp, self.lftq = lftp, lftq
13
+ def set_nested_interval(rational)
14
+ self.lftp, self.lftq = rational.numerator, rational.denominator
34
15
  self.rgtp = rgtp if has_attribute?(:rgtp)
35
16
  self.rgtq = rgtq if has_attribute?(:rgtq)
36
17
  self.lft = lft if has_attribute?(:lft)
37
18
  self.rgt = rgt if has_attribute?(:rgt)
38
19
  end
39
20
 
40
- def set_nested_interval_for_top
41
- if nested_interval.multiple_roots?
42
- set_nested_interval(*next_root_lft)
43
- else
44
- set_nested_interval 0, 1
45
- end
46
- end
47
-
48
21
  def nested_interval_scope
49
22
  conditions = {}
50
23
  nested_interval.scope_columns.each do |column_name|
@@ -53,80 +26,33 @@ module ActsAsNestedInterval
53
26
  self.class.where conditions
54
27
  end
55
28
 
56
- # Rewrite method
57
- def update_nested_interval_move
58
- return if self.class.readonly_attributes.include?(nested_interval.foreign_key.to_sym) # Fix issue #9
59
- begin
60
- db_self = self.class.find(id)
61
- db_parent = self.class.find(read_attribute(nested_interval.foreign_key))
62
- if db_self.ancestor_of?(db_parent)
63
- errors.add nested_interval.foreign_key, "is descendant"
64
- raise ActiveRecord::RecordInvalid, self
65
- end
66
- rescue ActiveRecord::RecordNotFound => e # root
29
+ def recalculate_nested_interval!
30
+ move! do
31
+ lftr = parent.present? ? parent.next_child_lft : next_root_lft
32
+ set_nested_interval( lftr )
33
+ save!
34
+ self.recalculate_nested_interval!
35
+ children.preorder.map(&:recalculate_nested_interval!)
67
36
  end
68
-
69
- if read_attribute(nested_interval.foreign_key).nil? # root move
70
- set_nested_interval_for_top
71
- else # child move
72
- set_nested_interval *parent.next_child_lft
73
- end
74
- cpp = db_self.lftq * rgtp - db_self.rgtq * lftp
75
- cpq = db_self.rgtp * lftp - db_self.lftp * rgtp
76
- cqp = db_self.lftq * rgtq - db_self.rgtq * lftq
77
- cqq = db_self.rgtp * lftq - db_self.lftp * rgtq
37
+ end
78
38
 
79
- updates = {}
80
- vars = Set.new
81
- # TODO
82
- mysql = false #["MySQL", "Mysql2"].include?(connection.adapter_name)
83
- var = ->(v) { mysql ? vars.add?(v) ? "(@#{v} := #{v})" : "@#{v}" : v }
84
- multiply = ->(c, b) { "#{c} * #{var.(b)}" }
85
- add = ->(a, b) { "#{a} + #{b}" }
86
- one = sprintf("%#.30f", 1)
87
- divide = ->(p, q) { "#{one} * (#{p}) / (#{q})" }
39
+ # Rewrite method
40
+ def update_nested_interval_move
41
+ return unless self.class.nested_interval.moveable?
88
42
 
89
- if has_attribute?(:rgtp) && has_attribute?(:rgtq)
90
- updates[:rgtp] = -> { add.(multiply.(cpp, :rgtp), multiply.(cpq, :rgtq)) }
91
- updates[:rgtq] = -> { add.(multiply.(cqp, :rgtp), multiply.(cqq, :rgtq)) }
92
- updates[:rgt] = -> { divide.(updates[:rgtp].(), updates[:rgtq].()) } if has_attribute?(:rgt)
43
+ if parent.present? and self.ancestor_of?(parent)
44
+ errors.add nested_interval.foreign_key, "is descendant"
45
+ raise ActiveRecord::RecordInvalid, self
93
46
  end
94
47
 
95
- updates[:lftp] = -> { add.(multiply.(cpp, :lftp), multiply.(cpq, :lftq)) }
96
- updates[:lftq] = -> { add.(multiply.(cqp, :lftp), multiply.(cqq, :lftq)) }
97
- updates[:lft] = -> { divide.(updates[:lftp].(), updates[:lftq].()) } if has_attribute?(:lft)
98
-
99
- sql = updates.map { |k, v| "#{k} = #{v.()}" }.join(', ')
100
-
101
- db_self.descendants.update_all sql
48
+ # TODO: Do it by DB
49
+ self.recalculate_nested_interval!
102
50
  end
103
51
 
104
- def ancestor_of?(node)
105
- node.lftp == lftp && node.lftq == lftq ||
106
- node.lftp > node.lftq * lftp / lftq &&
107
- node.lftp <= node.lftq * rgtp / rgtq &&
108
- (node.lftp != rgtp || node.lftq != rgtq)
109
- end
110
-
111
- def ancestors
112
- nested_interval_scope.where("rgt >= CAST(:rgt AS FLOAT) AND lft < CAST(:lft AS FLOAT)", rgt: rgt, lft: lft)
113
- end
114
-
115
- #def ancestors
116
- #sqls = ['0 = 1']
117
- #p, q = lftp, lftq
118
- #while p != 0
119
- #x = p.inverse(q)
120
- #p, q = (x * p - 1) / q, x
121
- #sqls << "lftq = #{q} AND lftp = #{p}"
122
- #end
123
- #nested_interval_scope.where(sqls * ' OR ')
124
- #end
125
-
126
52
  # Returns depth by counting ancestors up to 0 / 1.
127
53
  def depth
128
54
  if new_record?
129
- if parent_id.nil?
55
+ if parent.nil?
130
56
  return 0
131
57
  else
132
58
  return parent.depth + 1
@@ -167,22 +93,39 @@ module ActsAsNestedInterval
167
93
  # Returns left end of interval for next child.
168
94
  def next_child_lft
169
95
  if child = children.order('lftq DESC').first
170
- return lftp + child.lftp, lftq + child.lftq
96
+ return left.mediant( child.left )
171
97
  else
172
- return lftp + rgtp, lftq + rgtq
98
+ return left.mediant( right )
173
99
  end
174
100
  end
175
101
 
176
102
  # Returns left end of interval for next root.
177
103
  def next_root_lft
178
- vr = self.class.new # a virtual root
179
- vr.set_nested_interval 0, 1
180
- if child = nested_interval_scope.roots.order('lftq DESC').first
181
- return vr.lftp + child.lftp, vr.lftq + child.lftq
182
- else
183
- return vr.lftp + vr.rgtp, vr.lftq + vr.rgtq
184
- end
104
+ last_root = nested_interval_scope.roots.order( rgtp: :desc, rgtq: :desc ).first
105
+ raise Exception.new("Only one root allowed") if last_root.present? && !self.class.nested_interval.multiple_roots?
106
+ last_root.try(:right) || 0.to_r
185
107
  end
186
108
 
109
+ # Check if node is moved (parent changed)
110
+ def node_moved?
111
+ send(:"#{nested_interval.foreign_key}_changed?") # TODO: Check if parent moved?
112
+ end
113
+
114
+ def left
115
+ Rational(lftp, lftq)
116
+ end
117
+
118
+ def right
119
+ Rational(rgtp, rgtq)
120
+ end
121
+
122
+ protected
123
+
124
+ def disallow_circular_dependency
125
+ if parent == self
126
+ errors.add(self.class.nested_interval.foreign_key, 'cannot refer back to self')
127
+ end
128
+ end
129
+
187
130
  end
188
131
  end