acts_as_nested_interval 0.2.0 → 0.3.0.pre

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.
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