chronic_tree 1.0.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.
- checksums.yaml +7 -0
- data/.gitignore +19 -0
- data/.ruby-version +1 -0
- data/.travis.yml +7 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +87 -0
- data/LICENSE +21 -0
- data/README.md +278 -0
- data/Rakefile +6 -0
- data/chronic_tree.gemspec +30 -0
- data/lib/chronic_tree.rb +73 -0
- data/lib/chronic_tree/active_record/element.rb +26 -0
- data/lib/chronic_tree/active_record/relation.rb +34 -0
- data/lib/chronic_tree/command.rb +165 -0
- data/lib/chronic_tree/operation.rb +212 -0
- data/lib/chronic_tree/railtie.rb +9 -0
- data/lib/chronic_tree/travesal.rb +64 -0
- data/lib/chronic_tree/version.rb +3 -0
- data/lib/generators/chronic_tree/install_generator.rb +15 -0
- data/lib/generators/chronic_tree/templates/create_chronic_tree_elements.rb +22 -0
- data/spec/chronic_tree_spec.rb +223 -0
- data/spec/spec_helper.rb +438 -0
- metadata +179 -0
data/lib/chronic_tree.rb
ADDED
@@ -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
|