chronic_tree 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,73 @@
1
+ require "chronic_tree/version"
2
+ require "chronic_tree/active_record/element"
3
+ require "chronic_tree/travesal"
4
+ require "chronic_tree/operation"
5
+ require "chronic_tree/active_record/relation"
6
+ require "set"
7
+
8
+ module ChronicTree
9
+ require 'chronic_tree/railtie' if defined?(Rails)
10
+
11
+ class Error < RuntimeError; end
12
+ class InvalidObjectError < Error; end
13
+
14
+ include ChronicTree::ActiveRecord::Relation
15
+ include ChronicTree::Travesal
16
+ include ChronicTree::Operation
17
+
18
+ def self.included(base)
19
+ base.class_eval <<-RUBY
20
+ attr_reader :current_time_at, :current_scope_name
21
+
22
+ @@defined_chronic_tree_scopes = Set.new
23
+
24
+ def self.defined_chronic_tree_scopes
25
+ @@defined_chronic_tree_scopes
26
+ end
27
+ RUBY
28
+
29
+ base.extend(ClassMethods)
30
+ end
31
+
32
+ module ClassMethods
33
+
34
+ def chronic_tree(scope_name = 'default')
35
+ self.class_eval <<-RUBY
36
+ has_many :"elements_under_#{scope_name}_parent",
37
+ proc { |owner| where(scope_name: scope_name, tree_type: owner.class.name) },
38
+ class_name: 'ChronicTree::ActiveRecord::Element',
39
+ foreign_key: 'parent_id',
40
+ dependent: :destroy
41
+
42
+ has_many :"elements_as_#{scope_name}_child",
43
+ proc { |owner| where(scope_name: scope_name, tree_type: owner.class.name) },
44
+ class_name: 'ChronicTree::ActiveRecord::Element',
45
+ foreign_key: 'child_id',
46
+ dependent: :destroy
47
+
48
+ has_many :"elements_under_#{scope_name}_root",
49
+ proc { |owner| where(scope_name: scope_name, tree_type: owner.class.name) },
50
+ class_name: 'ChronicTree::ActiveRecord::Element',
51
+ foreign_key: 'root_id',
52
+ dependent: :destroy
53
+ RUBY
54
+
55
+ defined_chronic_tree_scopes << scope_name
56
+ end
57
+
58
+ end
59
+
60
+ def as_tree(time_at = Time.now, scope_name = 'default')
61
+ time_at, scope_name = Time.now, time_at if time_at.is_a?(String)
62
+
63
+ raise "Scope name is wrong. It should be equal to the name that has " \
64
+ "been set up." unless self.class.defined_chronic_tree_scopes.include?(scope_name)
65
+
66
+ raise "Time can't be later than now." if time_at > Time.now
67
+
68
+ @current_time_at = time_at
69
+ @current_scope_name = scope_name
70
+
71
+ self
72
+ end
73
+ end
@@ -0,0 +1,26 @@
1
+ module ChronicTree
2
+ module ActiveRecord
3
+ class Element < ::ActiveRecord::Base
4
+ self.table_name = "chronic_tree_elements"
5
+
6
+ belongs_to :parent, polymorphic: true, foreign_type: 'tree_type'
7
+ belongs_to :child, polymorphic: true, foreign_type: 'tree_type'
8
+ belongs_to :root, polymorphic: true, foreign_type: 'tree_type'
9
+
10
+ scope :at, -> (time = Time.now) {
11
+ start_time_col = self.arel_table[:start_time]
12
+ end_time_col = self.arel_table[:end_time]
13
+ where(start_time_col.lteq(time)).where(end_time_col.gt(time))
14
+ }
15
+
16
+ scope :exclude_root, -> {
17
+ root_id_col = self.arel_table[:root_id]
18
+ child_id_col = self.arel_table[:child_id]
19
+ where(root_id_col.not_eq(child_id_col))
20
+ }
21
+
22
+ scope :direct, -> { where(distance: 1) }
23
+ scope :all_distance, -> { unscope(where: :distance) }
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,34 @@
1
+ module ChronicTree
2
+ module ActiveRecord
3
+ module Relation
4
+ def children_relation(time_at, scope_name)
5
+ send("elements_under_#{scope_name}_parent").
6
+ at(time_at).
7
+ direct.
8
+ exclude_root.
9
+ includes(:child)
10
+ end
11
+
12
+ def parent_relation(time_at, scope_name)
13
+ existed_relation(time_at, scope_name).
14
+ direct.
15
+ exclude_root
16
+ end
17
+
18
+ def existed_relation(time_at, scope_name)
19
+ send("elements_as_#{scope_name}_child").
20
+ at(time_at)
21
+ end
22
+
23
+ def descendants_relation(time_at, scope_name)
24
+ children_relation(time_at, scope_name).order(:distance).all_distance
25
+ end
26
+
27
+ def ancestors_relation(time_at, scope_name)
28
+ existed_relation(time_at, scope_name).
29
+ includes(:parent).
30
+ order(:distance)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,165 @@
1
+ module ChronicTree
2
+ module Command
3
+
4
+ class AddRootElement
5
+ attr_reader :source
6
+
7
+ def initialize(source)
8
+ @source = source
9
+ end
10
+
11
+ def do
12
+ source.send("elements_under_#{source.current_scope_name}_root").create(
13
+ child: source,
14
+ parent: source,
15
+ distance: 0,
16
+ start_time: source.current_time_at,
17
+ end_time: 1000.years.since(source.current_time_at)
18
+ )
19
+ end
20
+ end
21
+
22
+ class AddChildElement
23
+ attr_reader :source, :child_id, :distance, :source_root_id
24
+
25
+ def initialize(source, child_object_or_child_id, distance = 1, source_root_id = nil)
26
+ @source = source
27
+ @source_root_id = if source_root_id.nil?
28
+ source.root.id
29
+ else
30
+ source_root_id
31
+ end
32
+
33
+ @distance = distance
34
+
35
+ @child_id = if child_object_or_child_id.respond_to?(:id)
36
+ child_object_or_child_id.send(:id)
37
+ else
38
+ child_object_or_child_id
39
+ end
40
+ end
41
+
42
+ def do
43
+ source.send("elements_under_#{source.current_scope_name}_parent").create(
44
+ root_id: source_root_id,
45
+ child_id: child_id,
46
+ distance: distance,
47
+ start_time: source.current_time_at,
48
+ end_time: 1000.years.since(source.current_time_at)
49
+ )
50
+ end
51
+ end
52
+
53
+ class RemoveChildElementsFromAncestors
54
+ attr_reader :ancestor_ids, :child_ids, :source
55
+
56
+ def initialize(source, ancestor_ids, child_ids)
57
+ @source = source
58
+ @ancestor_ids = ancestor_ids
59
+ @child_ids = child_ids
60
+ end
61
+
62
+ def do
63
+ ChronicTree::ActiveRecord::Element.at(source.current_time_at).
64
+ where(tree_type: source.class.name).
65
+ where(scope_name: source.current_scope_name).
66
+ where(parent_id: ancestor_ids).
67
+ where(child_id: child_ids).
68
+ update_all(end_time: source.current_time_at)
69
+ end
70
+ end
71
+
72
+ class AddChildElementsToNewAncestors
73
+ attr_reader :source, :source_root_id, :new_ancestors, :child_elements
74
+
75
+ def initialize(source, source_root_id, new_ancestors, child_elements)
76
+ @source = source
77
+ @source_root_id = source_root_id
78
+ @new_ancestors = new_ancestors
79
+ @child_elements = child_elements
80
+ end
81
+
82
+ def do
83
+ new_ancestors.each_with_index do |parent_object, i|
84
+ child_elements.each do |e|
85
+ AddChildElement.new(parent_object, e.child_id, e.distance + i, source_root_id).do
86
+ end
87
+ end
88
+ end
89
+ end
90
+
91
+ class AddChildElementToOrigAncestors
92
+ attr_reader :source, :child
93
+
94
+ def initialize(source, child)
95
+ @source = source
96
+ @child = child
97
+ end
98
+
99
+ def do
100
+ source.ancestors.each_with_index do |parent_object, index|
101
+ AddChildElement.new(parent_object, child, index + 2, source.root.id).do
102
+ end
103
+ end
104
+ end
105
+
106
+ class AddReplacedObjToOrigAncestors
107
+ attr_reader :source, :source_root_id, :source_ancestors, :target
108
+
109
+ def initialize(source, source_root_id, source_ancestors, target)
110
+ @source = source
111
+ @source_root_id = source_root_id
112
+ @source_ancestors = source_ancestors
113
+ @target = target
114
+ end
115
+
116
+ def do
117
+ source_ancestors.each_with_index do |parent_object, i|
118
+ AddChildElement.new(parent_object, target.id, i + 1, source_root_id).do
119
+ end
120
+ end
121
+ end
122
+
123
+ class AddChildElementsToReplacedObject
124
+ attr_reader :source, :source_root_id, :target, :child_elements
125
+
126
+ def initialize(source, source_root_id, target, child_elements)
127
+ @source = source
128
+ @source_root_id = source_root_id
129
+ @target = target
130
+ @child_elements = child_elements
131
+ end
132
+
133
+ def do
134
+ child_elements.each { |el| AddChildElement.new(target, el.child_id, el.distance).do }
135
+ end
136
+ end
137
+
138
+ class RemoveSelfElement
139
+ attr_reader :source
140
+
141
+ def initialize(source)
142
+ @source = source
143
+ end
144
+
145
+ def do
146
+ source.existed_relation(source.current_time_at, source.current_scope_name).each do |el|
147
+ el.update_attribute(:end_time, source.current_time_at)
148
+ end
149
+ end
150
+ end
151
+
152
+ class RemoveDescendantElements
153
+ attr_reader :source
154
+
155
+ def initialize(source)
156
+ @source = source
157
+ end
158
+
159
+ def do
160
+ source.flat_descendants.each { |object| RemoveSelfElement.new(object).do }
161
+ end
162
+ end
163
+
164
+ end
165
+ end
@@ -0,0 +1,212 @@
1
+ require 'chronic_tree/command'
2
+
3
+ module ChronicTree
4
+ module Operation
5
+
6
+ class ChangeParent
7
+ attr_reader :source, :source_root_id, :ready_to_move_elements, :new_ancestors
8
+
9
+ def initialize(source, new_parent_object)
10
+ @source = source
11
+
12
+ @ready_to_move_elements = source.
13
+ descendants_relation(source.current_time_at,
14
+ source.current_scope_name).map do |el|
15
+
16
+ OpenStruct.new(child_id: el.child_id, distance: el.distance + 1)
17
+ end
18
+ @ready_to_move_elements << OpenStruct.new(child_id: source.id, distance: 1)
19
+
20
+ @source_root_id = source.root.id
21
+
22
+ @new_ancestors = new_parent_object.ancestors
23
+ @new_ancestors.unshift(new_parent_object) unless new_parent_object.id == @source_root_id
24
+ end
25
+
26
+ def act
27
+ ::ActiveRecord::Base.transaction do
28
+ ChronicTree::Command::RemoveChildElementsFromAncestors.new(
29
+ source,
30
+ source.ancestors.map(&:id),
31
+ ready_to_move_elements.map(&:child_id)).do
32
+
33
+ ChronicTree::Command::AddChildElementsToNewAncestors.new(
34
+ source,
35
+ source_root_id,
36
+ new_ancestors,
37
+ ready_to_move_elements).do
38
+ end
39
+
40
+ source
41
+ end
42
+ end
43
+
44
+ class ReplaceBy
45
+ attr_reader :source, :target, :source_root_id, :source_ancestors, :ready_to_move_elements
46
+
47
+ def initialize(source, target)
48
+ @source = source
49
+ @target = target
50
+
51
+ @ready_to_move_elements = source.descendants_relation(
52
+ source.current_time_at,
53
+ source.current_scope_name).map do |el|
54
+
55
+ OpenStruct.new(id: el.id, child_id: el.child_id, distance: el.distance)
56
+ end
57
+
58
+ @source_root_id = (source == source.root) ? target.id : source.root.id
59
+ @source_ancestors = source.ancestors
60
+ end
61
+
62
+ def act
63
+ ::ActiveRecord::Base.transaction do
64
+ remove_obsolete_elements
65
+ add_new_elements
66
+ end
67
+
68
+ source
69
+ end
70
+
71
+ private
72
+
73
+ def remove_obsolete_elements
74
+ ChronicTree::Command::RemoveChildElementsFromAncestors.new(
75
+ source,
76
+ [source.id],
77
+ ready_to_move_elements.map(&:child_id)).do
78
+
79
+ ChronicTree::Command::RemoveSelfElement.new(source).do
80
+ end
81
+
82
+ def add_new_elements
83
+ if source.id == source_root_id
84
+ ChronicTree::Command::AddRootElement.new(source).do
85
+ else
86
+ ChronicTree::Command::AddReplacedObjToOrigAncestors.new(
87
+ source, source_root_id, source_ancestors, target).do
88
+ end
89
+
90
+ ChronicTree::Command::AddChildElementsToReplacedObject.new(
91
+ source, source_root_id, target, ready_to_move_elements).do
92
+ end
93
+ end
94
+
95
+
96
+
97
+ def add_as_root
98
+ as_tree(Time.now, current_scope_name || 'default')
99
+
100
+ raise_if_tree_is_not_empty
101
+
102
+ ChronicTree::Command::AddRootElement.new(self).do
103
+
104
+ self
105
+ end
106
+
107
+ def add_child(object)
108
+ as_tree(Time.now, current_scope_name || 'default') && object.as_tree(current_time_at, current_scope_name)
109
+
110
+ raise_if_object_unmatched(object)
111
+ raise_if_object_is_in_the_tree(object)
112
+ raise_if_self_is_not_in_the_tree
113
+
114
+ ::ActiveRecord::Base.transaction do
115
+ ChronicTree::Command::AddChildElement.new(self, object).do
116
+ ChronicTree::Command::AddChildElementToOrigAncestors.new(self, object).do unless self == root
117
+ end
118
+
119
+ self
120
+ end
121
+
122
+ def remove_self
123
+ as_tree(Time.now, current_scope_name || 'default')
124
+
125
+ raise_if_self_is_not_in_the_tree
126
+
127
+ ::ActiveRecord::Base.transaction do
128
+ ChronicTree::Command::RemoveSelfElement.new(self).do
129
+ ChronicTree::Command::RemoveDescendantElements.new(self).do
130
+ end
131
+
132
+ self
133
+ end
134
+
135
+ def remove_descendants
136
+ as_tree(Time.now, current_scope_name || 'default')
137
+
138
+ raise_if_self_is_not_in_the_tree
139
+
140
+ ::ActiveRecord::Base.transaction { ChronicTree::Command::RemoveDescendantElements.new(self).do }
141
+
142
+ self
143
+ end
144
+
145
+ def change_parent(object)
146
+ as_tree(Time.now, current_scope_name || 'default') && object.as_tree(current_time_at, current_scope_name)
147
+ return self if self != root && parent == object
148
+
149
+ raise_if_object_unmatched(object)
150
+ raise_if_object_is_not_in_the_tree(object)
151
+ raise_if_object_equals_to_self(object)
152
+ raise_if_object_is_a_child_of_self(object)
153
+ raise_if_self_is_not_in_the_tree
154
+
155
+ ChangeParent.new(self, object).act
156
+ end
157
+
158
+ def replace_by(object)
159
+ as_tree(Time.now, current_scope_name || 'default') && object.as_tree(current_time_at, current_scope_name)
160
+
161
+ raise_if_object_unmatched(object)
162
+ raise_if_object_is_in_the_tree(object)
163
+ raise_if_self_is_not_in_the_tree
164
+
165
+ ReplaceBy.new(self, object).act
166
+ end
167
+
168
+ private
169
+
170
+ def raise_if_object_unmatched(object)
171
+ if object.class.name != self.class.name
172
+ raise InvalidObjectError, "Object invalid. You can't add two types of objects in a tree."
173
+ end
174
+
175
+ raise InvalidObjectError, "Object invalid. You must save it first." if object.new_record?
176
+ end
177
+
178
+ def raise_if_object_is_in_the_tree(object)
179
+ if object.existed_in_tree?(current_time_at, current_scope_name)
180
+ raise InvalidObjectError, "Object must not be in the tree now."
181
+ end
182
+ end
183
+
184
+ def raise_if_object_is_not_in_the_tree(object)
185
+ unless object.existed_in_tree?(current_time_at, current_scope_name)
186
+ raise InvalidObjectError, "Object must be in the tree now."
187
+ end
188
+ end
189
+
190
+ def raise_if_object_equals_to_self(object)
191
+ raise InvalidObjectError, "Object can't be equal to self." if self == object
192
+ end
193
+
194
+ def raise_if_object_is_a_child_of_self(object)
195
+ if descendants_relation(current_time_at, current_scope_name).where(child_id: object.id).any?
196
+ raise InvalidObjectError, "Object can't be a child of self."
197
+ end
198
+ end
199
+
200
+ def raise_if_self_is_not_in_the_tree
201
+ unless existed_in_tree?(current_time_at, current_scope_name)
202
+ raise Error, "Self must be in the tree now."
203
+ end
204
+ end
205
+
206
+ def raise_if_tree_is_not_empty
207
+ unless tree_empty?(current_time_at, current_scope_name)
208
+ raise Error, "Tree isn't empty, can't add root element."
209
+ end
210
+ end
211
+ end
212
+ end