@@ -0,0 +1,195 | |||
|
1 | # Redmine - project management software | |
|
2 | # Copyright (C) 2006-2014 Jean-Philippe Lang | |
|
3 | # | |
|
4 | # This program is free software; you can redistribute it and/or | |
|
5 | # modify it under the terms of the GNU General Public License | |
|
6 | # as published by the Free Software Foundation; either version 2 | |
|
7 | # of the License, or (at your option) any later version. | |
|
8 | # | |
|
9 | # This program is distributed in the hope that it will be useful, | |
|
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
|
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
|
12 | # GNU General Public License for more details. | |
|
13 | # | |
|
14 | # You should have received a copy of the GNU General Public License | |
|
15 | # along with this program; if not, write to the Free Software | |
|
16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |
|
17 | ||
|
18 | module Redmine | |
|
19 | module NestedSet | |
|
20 | module IssueNestedSet | |
|
21 | def self.included(base) | |
|
22 | base.class_eval do | |
|
23 | belongs_to :parent, :class_name => self.name | |
|
24 | ||
|
25 | before_create :add_to_nested_set, :if => lambda {|issue| issue.parent.present?} | |
|
26 | after_create :add_as_root, :if => lambda {|issue| issue.parent.blank?} | |
|
27 | before_update :handle_parent_change, :if => lambda {|issue| issue.parent_id_changed?} | |
|
28 | before_destroy :destroy_children | |
|
29 | end | |
|
30 | base.extend ClassMethods | |
|
31 | base.send :include, Redmine::NestedSet::Traversing | |
|
32 | end | |
|
33 | ||
|
34 | private | |
|
35 | ||
|
36 | def target_lft | |
|
37 | scope_for_max_rgt = self.class.where(:root_id => root_id).where(:parent_id => parent_id) | |
|
38 | if id | |
|
39 | #scope_for_max_rgt = scope_for_max_rgt.where("id < ?", id) | |
|
40 | end | |
|
41 | max_rgt = scope_for_max_rgt.maximum(:rgt) | |
|
42 | if max_rgt | |
|
43 | max_rgt + 1 | |
|
44 | elsif parent | |
|
45 | parent.lft + 1 | |
|
46 | else | |
|
47 | 1 | |
|
48 | end | |
|
49 | end | |
|
50 | ||
|
51 | def add_to_nested_set(lock=true) | |
|
52 | lock_nested_set if lock | |
|
53 | parent.send :reload_nested_set_values | |
|
54 | self.root_id = parent.root_id | |
|
55 | self.lft = target_lft | |
|
56 | self.rgt = lft + 1 | |
|
57 | self.class.where(:root_id => root_id).where("lft >= ? OR rgt >= ?", lft, lft).update_all([ | |
|
58 | "lft = CASE WHEN lft >= :lft THEN lft + 2 ELSE lft END, " + | |
|
59 | "rgt = CASE WHEN rgt >= :lft THEN rgt + 2 ELSE rgt END", | |
|
60 | {:lft => lft} | |
|
61 | ]) | |
|
62 | end | |
|
63 | ||
|
64 | def add_as_root | |
|
65 | self.root_id = id | |
|
66 | self.lft = 1 | |
|
67 | self.rgt = 2 | |
|
68 | self.class.where(:id => id).update_all(:root_id => root_id, :lft => lft, :rgt => rgt) | |
|
69 | end | |
|
70 | ||
|
71 | def handle_parent_change | |
|
72 | lock_nested_set | |
|
73 | reload_nested_set_values | |
|
74 | if parent_id_was | |
|
75 | remove_from_nested_set | |
|
76 | end | |
|
77 | if parent | |
|
78 | move_to_nested_set | |
|
79 | end | |
|
80 | reload_nested_set_values | |
|
81 | end | |
|
82 | ||
|
83 | def move_to_nested_set | |
|
84 | if parent | |
|
85 | previous_root_id = root_id | |
|
86 | self.root_id = parent.root_id | |
|
87 | ||
|
88 | lft_after_move = target_lft | |
|
89 | self.class.where(:root_id => parent.root_id).update_all([ | |
|
90 | "lft = CASE WHEN lft >= :lft THEN lft + :shift ELSE lft END, " + | |
|
91 | "rgt = CASE WHEN rgt >= :lft THEN rgt + :shift ELSE rgt END", | |
|
92 | {:lft => lft_after_move, :shift => (rgt - lft + 1)} | |
|
93 | ]) | |
|
94 | ||
|
95 | self.class.where(:root_id => previous_root_id).update_all([ | |
|
96 | "root_id = :root_id, lft = lft + :shift, rgt = rgt + :shift", | |
|
97 | {:root_id => parent.root_id, :shift => lft_after_move - lft} | |
|
98 | ]) | |
|
99 | ||
|
100 | self.lft, self.rgt = lft_after_move, (rgt - lft + lft_after_move) | |
|
101 | parent.send :reload_nested_set_values | |
|
102 | end | |
|
103 | end | |
|
104 | ||
|
105 | def remove_from_nested_set | |
|
106 | self.class.where(:root_id => root_id).where("lft >= ? AND rgt <= ?", lft, rgt). | |
|
107 | update_all(["root_id = :id, lft = lft - :shift, rgt = rgt - :shift", {:id => id, :shift => lft - 1}]) | |
|
108 | ||
|
109 | self.class.where(:root_id => root_id).update_all([ | |
|
110 | "lft = CASE WHEN lft >= :lft THEN lft - :shift ELSE lft END, " + | |
|
111 | "rgt = CASE WHEN rgt >= :lft THEN rgt - :shift ELSE rgt END", | |
|
112 | {:lft => lft, :shift => rgt - lft + 1} | |
|
113 | ]) | |
|
114 | self.root_id = id | |
|
115 | self.lft, self.rgt = 1, (rgt - lft + 1) | |
|
116 | end | |
|
117 | ||
|
118 | def destroy_children | |
|
119 | unless @without_nested_set_update | |
|
120 | lock_nested_set | |
|
121 | reload_nested_set_values | |
|
122 | end | |
|
123 | children.each {|c| c.send :destroy_without_nested_set_update} | |
|
124 | reload | |
|
125 | unless @without_nested_set_update | |
|
126 | self.class.where(:root_id => root_id).where("lft > ? OR rgt > ?", lft, lft).update_all([ | |
|
127 | "lft = CASE WHEN lft > :lft THEN lft - :shift ELSE lft END, " + | |
|
128 | "rgt = CASE WHEN rgt > :lft THEN rgt - :shift ELSE rgt END", | |
|
129 | {:lft => lft, :shift => rgt - lft + 1} | |
|
130 | ]) | |
|
131 | end | |
|
132 | end | |
|
133 | ||
|
134 | def destroy_without_nested_set_update | |
|
135 | @without_nested_set_update = true | |
|
136 | destroy | |
|
137 | end | |
|
138 | ||
|
139 | def reload_nested_set_values | |
|
140 | self.root_id, self.lft, self.rgt = self.class.where(:id => id).pluck(:root_id, :lft, :rgt).first | |
|
141 | end | |
|
142 | ||
|
143 | def save_nested_set_values | |
|
144 | self.class.where(:id => id).update_all(:root_id => root_id, :lft => lft, :rgt => rgt) | |
|
145 | end | |
|
146 | ||
|
147 | def move_possible?(issue) | |
|
148 | !is_or_is_ancestor_of?(issue) | |
|
149 | end | |
|
150 | ||
|
151 | def lock_nested_set | |
|
152 | lock = true | |
|
153 | if self.class.connection.adapter_name =~ /sqlserver/i | |
|
154 | lock = "WITH (ROWLOCK HOLDLOCK UPDLOCK)" | |
|
155 | end | |
|
156 | sets_to_lock = [id, parent_id].compact | |
|
157 | self.class.reorder(:id).where("root_id IN (SELECT root_id FROM #{self.class.table_name} WHERE id IN (?))", sets_to_lock).lock(lock).ids | |
|
158 | end | |
|
159 | ||
|
160 | def nested_set_scope | |
|
161 | self.class.order(:lft).where(:root_id => root_id) | |
|
162 | end | |
|
163 | ||
|
164 | def same_nested_set_scope?(issue) | |
|
165 | root_id == issue.root_id | |
|
166 | end | |
|
167 | ||
|
168 | module ClassMethods | |
|
169 | def rebuild_tree! | |
|
170 | transaction do | |
|
171 | reorder(:id).lock.ids | |
|
172 | update_all(:root_id => nil, :lft => nil, :rgt => nil) | |
|
173 | where(:parent_id => nil).update_all(["root_id = id, lft = ?, rgt = ?", 1, 2]) | |
|
174 | roots_with_children = joins("JOIN #{table_name} parent ON parent.id = #{table_name}.parent_id AND parent.id = parent.root_id").uniq.pluck("parent.id") | |
|
175 | roots_with_children.each do |root_id| | |
|
176 | rebuild_nodes(root_id) | |
|
177 | end | |
|
178 | end | |
|
179 | end | |
|
180 | ||
|
181 | private | |
|
182 | ||
|
183 | def rebuild_nodes(parent_id = nil) | |
|
184 | nodes = where(:parent_id => parent_id, :rgt => nil, :lft => nil).order(:id).to_a | |
|
185 | ||
|
186 | nodes.each do |node| | |
|
187 | node.send :add_to_nested_set, false | |
|
188 | node.send :save_nested_set_values | |
|
189 | rebuild_nodes node.id | |
|
190 | end | |
|
191 | end | |
|
192 | end | |
|
193 | end | |
|
194 | end | |
|
195 | end |
@@ -0,0 +1,159 | |||
|
1 | # Redmine - project management software | |
|
2 | # Copyright (C) 2006-2014 Jean-Philippe Lang | |
|
3 | # | |
|
4 | # This program is free software; you can redistribute it and/or | |
|
5 | # modify it under the terms of the GNU General Public License | |
|
6 | # as published by the Free Software Foundation; either version 2 | |
|
7 | # of the License, or (at your option) any later version. | |
|
8 | # | |
|
9 | # This program is distributed in the hope that it will be useful, | |
|
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
|
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
|
12 | # GNU General Public License for more details. | |
|
13 | # | |
|
14 | # You should have received a copy of the GNU General Public License | |
|
15 | # along with this program; if not, write to the Free Software | |
|
16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |
|
17 | ||
|
18 | module Redmine | |
|
19 | module NestedSet | |
|
20 | module ProjectNestedSet | |
|
21 | def self.included(base) | |
|
22 | base.class_eval do | |
|
23 | belongs_to :parent, :class_name => self.name | |
|
24 | ||
|
25 | before_create :add_to_nested_set | |
|
26 | before_update :move_in_nested_set, :if => lambda {|project| project.parent_id_changed? || project.name_changed?} | |
|
27 | before_destroy :destroy_children | |
|
28 | end | |
|
29 | base.extend ClassMethods | |
|
30 | base.send :include, Redmine::NestedSet::Traversing | |
|
31 | end | |
|
32 | ||
|
33 | private | |
|
34 | ||
|
35 | def target_lft | |
|
36 | siblings_rgt = self.class.where(:parent_id => parent_id).where("name < ?", name).maximum(:rgt) | |
|
37 | if siblings_rgt | |
|
38 | siblings_rgt + 1 | |
|
39 | elsif parent_id | |
|
40 | parent_lft = self.class.where(:id => parent_id).pluck(:lft).first | |
|
41 | raise "Project id=#{id} with parent_id=#{parent_id}: parent missing or without 'lft' value" unless parent_lft | |
|
42 | parent_lft + 1 | |
|
43 | else | |
|
44 | 1 | |
|
45 | end | |
|
46 | end | |
|
47 | ||
|
48 | def add_to_nested_set(lock=true) | |
|
49 | lock_nested_set if lock | |
|
50 | self.lft = target_lft | |
|
51 | self.rgt = lft + 1 | |
|
52 | self.class.where("lft >= ? OR rgt >= ?", lft, lft).update_all([ | |
|
53 | "lft = CASE WHEN lft >= :lft THEN lft + 2 ELSE lft END, " + | |
|
54 | "rgt = CASE WHEN rgt >= :lft THEN rgt + 2 ELSE rgt END", | |
|
55 | {:lft => lft} | |
|
56 | ]) | |
|
57 | end | |
|
58 | ||
|
59 | def move_in_nested_set | |
|
60 | lock_nested_set | |
|
61 | reload_nested_set_values | |
|
62 | a = lft | |
|
63 | b = rgt | |
|
64 | c = target_lft | |
|
65 | unless c == a | |
|
66 | if c > a | |
|
67 | # Moving to the right | |
|
68 | d = c - (b - a + 1) | |
|
69 | scope = self.class.where(["lft BETWEEN :a AND :c - 1 OR rgt BETWEEN :a AND :c - 1", {:a => a, :c => c}]) | |
|
70 | scope.update_all([ | |
|
71 | "lft = CASE WHEN lft BETWEEN :a AND :b THEN lft + (:d - :a) WHEN lft BETWEEN :b + 1 AND :c - 1 THEN lft - (:b - :a + 1) ELSE lft END, " + | |
|
72 | "rgt = CASE WHEN rgt BETWEEN :a AND :b THEN rgt + (:d - :a) WHEN rgt BETWEEN :b + 1 AND :c - 1 THEN rgt - (:b - :a + 1) ELSE rgt END", | |
|
73 | {:a => a, :b => b, :c => c, :d => d} | |
|
74 | ]) | |
|
75 | elsif c < a | |
|
76 | # Moving to the left | |
|
77 | scope = self.class.where("lft BETWEEN :c AND :b OR rgt BETWEEN :c AND :b", {:a => a, :b => b, :c => c}) | |
|
78 | scope.update_all([ | |
|
79 | "lft = CASE WHEN lft BETWEEN :a AND :b THEN lft - (:a - :c) WHEN lft BETWEEN :c AND :a - 1 THEN lft + (:b - :a + 1) ELSE lft END, " + | |
|
80 | "rgt = CASE WHEN rgt BETWEEN :a AND :b THEN rgt - (:a - :c) WHEN rgt BETWEEN :c AND :a - 1 THEN rgt + (:b - :a + 1) ELSE rgt END", | |
|
81 | {:a => a, :b => b, :c => c, :d => d} | |
|
82 | ]) | |
|
83 | end | |
|
84 | reload_nested_set_values | |
|
85 | end | |
|
86 | end | |
|
87 | ||
|
88 | def destroy_children | |
|
89 | unless @without_nested_set_update | |
|
90 | lock_nested_set | |
|
91 | reload_nested_set_values | |
|
92 | end | |
|
93 | children.each {|c| c.send :destroy_without_nested_set_update} | |
|
94 | unless @without_nested_set_update | |
|
95 | self.class.where("lft > ? OR rgt > ?", lft, lft).update_all([ | |
|
96 | "lft = CASE WHEN lft > :lft THEN lft - :shift ELSE lft END, " + | |
|
97 | "rgt = CASE WHEN rgt > :lft THEN rgt - :shift ELSE rgt END", | |
|
98 | {:lft => lft, :shift => rgt - lft + 1} | |
|
99 | ]) | |
|
100 | end | |
|
101 | end | |
|
102 | ||
|
103 | def destroy_without_nested_set_update | |
|
104 | @without_nested_set_update = true | |
|
105 | destroy | |
|
106 | end | |
|
107 | ||
|
108 | def reload_nested_set_values | |
|
109 | self.lft, self.rgt = Project.where(:id => id).pluck(:lft, :rgt).first | |
|
110 | end | |
|
111 | ||
|
112 | def save_nested_set_values | |
|
113 | self.class.where(:id => id).update_all(:lft => lft, :rgt => rgt) | |
|
114 | end | |
|
115 | ||
|
116 | def move_possible?(project) | |
|
117 | !is_or_is_ancestor_of?(project) | |
|
118 | end | |
|
119 | ||
|
120 | def lock_nested_set | |
|
121 | lock = true | |
|
122 | if self.class.connection.adapter_name =~ /sqlserver/i | |
|
123 | lock = "WITH (ROWLOCK HOLDLOCK UPDLOCK)" | |
|
124 | end | |
|
125 | self.class.order(:id).lock(lock).ids | |
|
126 | end | |
|
127 | ||
|
128 | def nested_set_scope | |
|
129 | self.class.order(:lft) | |
|
130 | end | |
|
131 | ||
|
132 | def same_nested_set_scope?(project) | |
|
133 | true | |
|
134 | end | |
|
135 | ||
|
136 | module ClassMethods | |
|
137 | def rebuild_tree! | |
|
138 | transaction do | |
|
139 | reorder(:id).lock.ids | |
|
140 | update_all(:lft => nil, :rgt => nil) | |
|
141 | rebuild_nodes | |
|
142 | end | |
|
143 | end | |
|
144 | ||
|
145 | private | |
|
146 | ||
|
147 | def rebuild_nodes(parent_id = nil) | |
|
148 | nodes = Project.where(:parent_id => parent_id).where(:rgt => nil, :lft => nil).reorder(:name) | |
|
149 | ||
|
150 | nodes.each do |node| | |
|
151 | node.send :add_to_nested_set, false | |
|
152 | node.send :save_nested_set_values | |
|
153 | rebuild_nodes node.id | |
|
154 | end | |
|
155 | end | |
|
156 | end | |
|
157 | end | |
|
158 | end | |
|
159 | end |
@@ -0,0 +1,116 | |||
|
1 | # Redmine - project management software | |
|
2 | # Copyright (C) 2006-2014 Jean-Philippe Lang | |
|
3 | # | |
|
4 | # This program is free software; you can redistribute it and/or | |
|
5 | # modify it under the terms of the GNU General Public License | |
|
6 | # as published by the Free Software Foundation; either version 2 | |
|
7 | # of the License, or (at your option) any later version. | |
|
8 | # | |
|
9 | # This program is distributed in the hope that it will be useful, | |
|
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
|
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
|
12 | # GNU General Public License for more details. | |
|
13 | # | |
|
14 | # You should have received a copy of the GNU General Public License | |
|
15 | # along with this program; if not, write to the Free Software | |
|
16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |
|
17 | ||
|
18 | module Redmine | |
|
19 | module NestedSet | |
|
20 | module Traversing | |
|
21 | def self.included(base) | |
|
22 | base.class_eval do | |
|
23 | scope :roots, lambda {where :parent_id => nil} | |
|
24 | scope :leaves, lambda {where "#{table_name}.rgt - #{table_name}.lft = ?", 1} | |
|
25 | end | |
|
26 | end | |
|
27 | ||
|
28 | # Returns true if the element has no parent | |
|
29 | def root? | |
|
30 | parent_id.nil? | |
|
31 | end | |
|
32 | ||
|
33 | # Returns true if the element has a parent | |
|
34 | def child? | |
|
35 | !root? | |
|
36 | end | |
|
37 | ||
|
38 | # Returns true if the element has no children | |
|
39 | def leaf? | |
|
40 | new_record? || (rgt - lft == 1) | |
|
41 | end | |
|
42 | ||
|
43 | # Returns the root element (ancestor with no parent) | |
|
44 | def root | |
|
45 | self_and_ancestors.first | |
|
46 | end | |
|
47 | ||
|
48 | # Returns the children | |
|
49 | def children | |
|
50 | if id.nil? | |
|
51 | nested_set_scope.none | |
|
52 | else | |
|
53 | self.class.order(:lft).where(:parent_id => id) | |
|
54 | end | |
|
55 | end | |
|
56 | ||
|
57 | # Returns the descendants that have no children | |
|
58 | def leaves | |
|
59 | descendants.where("#{self.class.table_name}.rgt - #{self.class.table_name}.lft = ?", 1) | |
|
60 | end | |
|
61 | ||
|
62 | # Returns the siblings | |
|
63 | def siblings | |
|
64 | nested_set_scope.where(:parent_id => parent_id).where("id <> ?", id) | |
|
65 | end | |
|
66 | ||
|
67 | # Returns the ancestors | |
|
68 | def ancestors | |
|
69 | if root? | |
|
70 | nested_set_scope.none | |
|
71 | else | |
|
72 | nested_set_scope.where("#{self.class.table_name}.lft < ? AND #{self.class.table_name}.rgt > ?", lft, rgt) | |
|
73 | end | |
|
74 | end | |
|
75 | ||
|
76 | # Returns the element and its ancestors | |
|
77 | def self_and_ancestors | |
|
78 | nested_set_scope.where("#{self.class.table_name}.lft <= ? AND #{self.class.table_name}.rgt >= ?", lft, rgt) | |
|
79 | end | |
|
80 | ||
|
81 | # Returns true if the element is an ancestor of other | |
|
82 | def is_ancestor_of?(other) | |
|
83 | same_nested_set_scope?(other) && other.lft > lft && other.rgt < rgt | |
|
84 | end | |
|
85 | ||
|
86 | # Returns true if the element equals other or is an ancestor of other | |
|
87 | def is_or_is_ancestor_of?(other) | |
|
88 | other == self || is_ancestor_of?(other) | |
|
89 | end | |
|
90 | ||
|
91 | # Returns the descendants | |
|
92 | def descendants | |
|
93 | if leaf? | |
|
94 | nested_set_scope.none | |
|
95 | else | |
|
96 | nested_set_scope.where("#{self.class.table_name}.lft > ? AND #{self.class.table_name}.rgt < ?", lft, rgt) | |
|
97 | end | |
|
98 | end | |
|
99 | ||
|
100 | # Returns the element and its descendants | |
|
101 | def self_and_descendants | |
|
102 | nested_set_scope.where("#{self.class.table_name}.lft >= ? AND #{self.class.table_name}.rgt <= ?", lft, rgt) | |
|
103 | end | |
|
104 | ||
|
105 | # Returns true if the element is a descendant of other | |
|
106 | def is_descendant_of?(other) | |
|
107 | same_nested_set_scope?(other) && other.lft < lft && other.rgt > rgt | |
|
108 | end | |
|
109 | ||
|
110 | # Returns true if the element equals other or is a descendant of other | |
|
111 | def is_or_is_descendant_of?(other) | |
|
112 | other == self || is_descendant_of?(other) | |
|
113 | end | |
|
114 | end | |
|
115 | end | |
|
116 | end |
@@ -0,0 +1,73 | |||
|
1 | # Redmine - project management software | |
|
2 | # Copyright (C) 2006-2014 Jean-Philippe Lang | |
|
3 | # | |
|
4 | # This program is free software; you can redistribute it and/or | |
|
5 | # modify it under the terms of the GNU General Public License | |
|
6 | # as published by the Free Software Foundation; either version 2 | |
|
7 | # of the License, or (at your option) any later version. | |
|
8 | # | |
|
9 | # This program is distributed in the hope that it will be useful, | |
|
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
|
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
|
12 | # GNU General Public License for more details. | |
|
13 | # | |
|
14 | # You should have received a copy of the GNU General Public License | |
|
15 | # along with this program; if not, write to the Free Software | |
|
16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |
|
17 | ||
|
18 | require File.expand_path('../../test_helper', __FILE__) | |
|
19 | ||
|
20 | class IssueNestedSetConcurrencyTest < ActiveSupport::TestCase | |
|
21 | fixtures :projects, :users, | |
|
22 | :trackers, :projects_trackers, | |
|
23 | :enabled_modules, | |
|
24 | :issue_statuses, | |
|
25 | :enumerations | |
|
26 | ||
|
27 | self.use_transactional_fixtures = false | |
|
28 | ||
|
29 | def setup | |
|
30 | CustomField.delete_all | |
|
31 | end | |
|
32 | ||
|
33 | def teardown | |
|
34 | Issue.delete_all | |
|
35 | end | |
|
36 | ||
|
37 | def test_concurrency | |
|
38 | skip if sqlite? | |
|
39 | with_settings :notified_events => [] do | |
|
40 | # Generates an issue and destroys it in order | |
|
41 | # to load all needed classes before starting threads | |
|
42 | i = Issue.generate! | |
|
43 | i.destroy | |
|
44 | ||
|
45 | root = Issue.generate! | |
|
46 | assert_difference 'Issue.count', 60 do | |
|
47 | threads = [] | |
|
48 | 3.times do |i| | |
|
49 | threads << Thread.new(i) do | |
|
50 | ActiveRecord::Base.connection_pool.with_connection do | |
|
51 | begin | |
|
52 | 10.times do | |
|
53 | i = Issue.generate! :parent_issue_id => root.id | |
|
54 | c1 = Issue.generate! :parent_issue_id => i.id | |
|
55 | c2 = Issue.generate! :parent_issue_id => i.id | |
|
56 | c3 = Issue.generate! :parent_issue_id => i.id | |
|
57 | c2.reload.destroy | |
|
58 | c1.reload.destroy | |
|
59 | end | |
|
60 | rescue Exception => e | |
|
61 | Thread.current[:exception] = e.message | |
|
62 | end | |
|
63 | end | |
|
64 | end | |
|
65 | end | |
|
66 | threads.each do |thread| | |
|
67 | thread.join | |
|
68 | assert_nil thread[:exception] | |
|
69 | end | |
|
70 | end | |
|
71 | end | |
|
72 | end | |
|
73 | end |
@@ -0,0 +1,76 | |||
|
1 | # Redmine - project management software | |
|
2 | # Copyright (C) 2006-2014 Jean-Philippe Lang | |
|
3 | # | |
|
4 | # This program is free software; you can redistribute it and/or | |
|
5 | # modify it under the terms of the GNU General Public License | |
|
6 | # as published by the Free Software Foundation; either version 2 | |
|
7 | # of the License, or (at your option) any later version. | |
|
8 | # | |
|
9 | # This program is distributed in the hope that it will be useful, | |
|
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
|
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
|
12 | # GNU General Public License for more details. | |
|
13 | # | |
|
14 | # You should have received a copy of the GNU General Public License | |
|
15 | # along with this program; if not, write to the Free Software | |
|
16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |
|
17 | ||
|
18 | require File.expand_path('../../test_helper', __FILE__) | |
|
19 | ||
|
20 | class ProjectNestedSetConcurrencyTest < ActiveSupport::TestCase | |
|
21 | self.use_transactional_fixtures = false | |
|
22 | ||
|
23 | def setup | |
|
24 | CustomField.delete_all | |
|
25 | end | |
|
26 | ||
|
27 | def teardown | |
|
28 | Project.delete_all | |
|
29 | end | |
|
30 | ||
|
31 | def test_concurrency | |
|
32 | skip if sqlite? | |
|
33 | # Generates a project and destroys it in order | |
|
34 | # to load all needed classes before starting threads | |
|
35 | p = generate_project! | |
|
36 | p.destroy | |
|
37 | ||
|
38 | assert_difference 'Project.count', 60 do | |
|
39 | threads = [] | |
|
40 | 3.times do |i| | |
|
41 | threads << Thread.new(i) do | |
|
42 | ActiveRecord::Base.connection_pool.with_connection do | |
|
43 | begin | |
|
44 | 10.times do | |
|
45 | p = generate_project! | |
|
46 | c1 = generate_project! :parent_id => p.id | |
|
47 | c2 = generate_project! :parent_id => p.id | |
|
48 | c3 = generate_project! :parent_id => p.id | |
|
49 | c2.reload.destroy | |
|
50 | c1.reload.destroy | |
|
51 | end | |
|
52 | rescue Exception => e | |
|
53 | Thread.current[:exception] = e.message | |
|
54 | end | |
|
55 | end | |
|
56 | end | |
|
57 | end | |
|
58 | threads.each do |thread| | |
|
59 | thread.join | |
|
60 | assert_nil thread[:exception] | |
|
61 | end | |
|
62 | end | |
|
63 | end | |
|
64 | ||
|
65 | # Generates a bare project with random name | |
|
66 | # and identifier | |
|
67 | def generate_project!(attributes={}) | |
|
68 | identifier = "a"+Redmine::Utils.random_hex(6) | |
|
69 | Project.generate!({ | |
|
70 | :identifier => identifier, | |
|
71 | :name => identifier, | |
|
72 | :tracker_ids => [], | |
|
73 | :enabled_module_names => [] | |
|
74 | }.merge(attributes)) | |
|
75 | end | |
|
76 | end |
@@ -6,7 +6,6 gem "coderay", "~> 1.1.0" | |||
|
6 | 6 | gem "builder", ">= 3.0.4" |
|
7 | 7 | gem "request_store", "1.0.5" |
|
8 | 8 | gem "mime-types" |
|
9 | gem "awesome_nested_set", "3.0.0" | |
|
10 | 9 | gem "protected_attributes" |
|
11 | 10 | gem "actionpack-action_caching" |
|
12 | 11 | gem "actionpack-xml_parser" |
@@ -19,6 +19,8 class Issue < ActiveRecord::Base | |||
|
19 | 19 | include Redmine::SafeAttributes |
|
20 | 20 | include Redmine::Utils::DateCalculation |
|
21 | 21 | include Redmine::I18n |
|
22 | before_save :set_parent_id | |
|
23 | include Redmine::NestedSet::IssueNestedSet | |
|
22 | 24 | |
|
23 | 25 | belongs_to :project |
|
24 | 26 | belongs_to :tracker |
@@ -41,7 +43,6 class Issue < ActiveRecord::Base | |||
|
41 | 43 | has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all |
|
42 | 44 | has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all |
|
43 | 45 | |
|
44 | acts_as_nested_set :scope => 'root_id', :dependent => :destroy | |
|
45 | 46 | acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed |
|
46 | 47 | acts_as_customizable |
|
47 | 48 | acts_as_watchable |
@@ -185,7 +186,7 class Issue < ActiveRecord::Base | |||
|
185 | 186 | # the lock_version condition should not be an issue but we handle it. |
|
186 | 187 | def destroy |
|
187 | 188 | super |
|
188 | rescue ActiveRecord::RecordNotFound | |
|
189 | rescue ActiveRecord::StaleObjectError, ActiveRecord::RecordNotFound | |
|
189 | 190 | # Stale or already deleted |
|
190 | 191 | begin |
|
191 | 192 | reload |
@@ -615,10 +616,8 class Issue < ActiveRecord::Base | |||
|
615 | 616 | errors.add :parent_issue_id, :invalid |
|
616 | 617 | elsif !new_record? |
|
617 | 618 | # moving an existing issue |
|
618 | if @parent_issue.root_id != root_id | |
|
619 | # we can always move to another tree | |
|
620 | elsif move_possible?(@parent_issue) | |
|
621 | # move accepted inside tree | |
|
619 | if move_possible?(@parent_issue) | |
|
620 | # move accepted | |
|
622 | 621 | else |
|
623 | 622 | errors.add :parent_issue_id, :invalid |
|
624 | 623 | end |
@@ -1184,6 +1183,10 class Issue < ActiveRecord::Base | |||
|
1184 | 1183 | end |
|
1185 | 1184 | end |
|
1186 | 1185 | |
|
1186 | def set_parent_id | |
|
1187 | self.parent_id = parent_issue_id | |
|
1188 | end | |
|
1189 | ||
|
1187 | 1190 | # Returns true if issue's project is a valid |
|
1188 | 1191 | # parent issue project |
|
1189 | 1192 | def valid_parent_project?(issue=parent) |
@@ -1366,14 +1369,7 class Issue < ActiveRecord::Base | |||
|
1366 | 1369 | end |
|
1367 | 1370 | |
|
1368 | 1371 | def update_nested_set_attributes |
|
1369 |
if |
|
|
1370 | # issue was just created | |
|
1371 | self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id) | |
|
1372 | Issue.where(["id = ?", id]).update_all(["root_id = ?", root_id]) | |
|
1373 | if @parent_issue | |
|
1374 | move_to_child_of(@parent_issue) | |
|
1375 | end | |
|
1376 | elsif parent_issue_id != parent_id | |
|
1372 | if parent_id_changed? | |
|
1377 | 1373 | update_nested_set_attributes_on_parent_change |
|
1378 | 1374 | end |
|
1379 | 1375 | remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue) |
@@ -1381,31 +1377,7 class Issue < ActiveRecord::Base | |||
|
1381 | 1377 | |
|
1382 | 1378 | # Updates the nested set for when an existing issue is moved |
|
1383 | 1379 | def update_nested_set_attributes_on_parent_change |
|
1384 | former_parent_id = parent_id | |
|
1385 | # moving an existing issue | |
|
1386 | if @parent_issue && @parent_issue.root_id == root_id | |
|
1387 | # inside the same tree | |
|
1388 | move_to_child_of(@parent_issue) | |
|
1389 | else | |
|
1390 | # to another tree | |
|
1391 | unless root? | |
|
1392 | move_to_right_of(root) | |
|
1393 | end | |
|
1394 | old_root_id = root_id | |
|
1395 | in_tenacious_transaction do | |
|
1396 | @parent_issue.reload_nested_set if @parent_issue | |
|
1397 | self.reload_nested_set | |
|
1398 | self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id) | |
|
1399 | cond = ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt] | |
|
1400 | self.class.base_class.select('id').lock(true).where(cond) | |
|
1401 | offset = rdm_right_most_bound + 1 - lft | |
|
1402 | Issue.where(cond). | |
|
1403 | update_all(["root_id = ?, lft = lft + ?, rgt = rgt + ?", root_id, offset, offset]) | |
|
1404 | end | |
|
1405 | if @parent_issue | |
|
1406 | move_to_child_of(@parent_issue) | |
|
1407 | end | |
|
1408 | end | |
|
1380 | former_parent_id = parent_id_was | |
|
1409 | 1381 | # delete invalid relations of all descendants |
|
1410 | 1382 | self_and_descendants.each do |issue| |
|
1411 | 1383 | issue.relations.each do |relation| |
@@ -1416,16 +1388,11 class Issue < ActiveRecord::Base | |||
|
1416 | 1388 | recalculate_attributes_for(former_parent_id) if former_parent_id |
|
1417 | 1389 | end |
|
1418 | 1390 | |
|
1419 | def rdm_right_most_bound | |
|
1420 | right_most_node = | |
|
1421 | self.class.base_class.unscoped. | |
|
1422 | order("#{quoted_right_column_full_name} desc").limit(1).lock(true).first | |
|
1423 | right_most_node ? (right_most_node[right_column_name] || 0 ) : 0 | |
|
1424 | end | |
|
1425 | private :rdm_right_most_bound | |
|
1426 | ||
|
1427 | 1391 | def update_parent_attributes |
|
1428 | recalculate_attributes_for(parent_id) if parent_id | |
|
1392 | if parent_id | |
|
1393 | recalculate_attributes_for(parent_id) | |
|
1394 | association(:parent).reset | |
|
1395 | end | |
|
1429 | 1396 | end |
|
1430 | 1397 | |
|
1431 | 1398 | def recalculate_attributes_for(issue_id) |
@@ -17,6 +17,7 | |||
|
17 | 17 | |
|
18 | 18 | class Project < ActiveRecord::Base |
|
19 | 19 | include Redmine::SafeAttributes |
|
20 | include Redmine::NestedSet::ProjectNestedSet | |
|
20 | 21 | |
|
21 | 22 | # Project statuses |
|
22 | 23 | STATUS_ACTIVE = 1 |
@@ -58,7 +59,6 class Project < ActiveRecord::Base | |||
|
58 | 59 | :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}", |
|
59 | 60 | :association_foreign_key => 'custom_field_id' |
|
60 | 61 | |
|
61 | acts_as_nested_set :dependent => :destroy | |
|
62 | 62 | acts_as_attachable :view_permission => :view_files, |
|
63 | 63 | :edit_permission => :manage_files, |
|
64 | 64 | :delete_permission => :manage_files |
@@ -81,8 +81,8 class Project < ActiveRecord::Base | |||
|
81 | 81 | # reserved words |
|
82 | 82 | validates_exclusion_of :identifier, :in => %w( new ) |
|
83 | 83 | |
|
84 | after_save :update_position_under_parent, :if => Proc.new {|project| project.name_changed?} | |
|
85 | 84 | after_save :update_inherited_members, :if => Proc.new {|project| project.inherit_members_changed?} |
|
85 | after_save :remove_inherited_member_roles, :add_inherited_member_roles, :if => Proc.new {|project| project.parent_id_changed?} | |
|
86 | 86 | before_destroy :delete_all_members |
|
87 | 87 | |
|
88 | 88 | scope :has_module, lambda {|mod| |
@@ -414,7 +414,9 class Project < ActiveRecord::Base | |||
|
414 | 414 | # Nothing to do |
|
415 | 415 | true |
|
416 | 416 | elsif p.nil? || (p.active? && move_possible?(p)) |
|
417 | set_or_update_position_under(p) | |
|
417 | self.parent = p | |
|
418 | save | |
|
419 | p.reload if p | |
|
418 | 420 | Issue.update_versions_from_hierarchy_change(self) |
|
419 | 421 | true |
|
420 | 422 | else |
@@ -423,17 +425,6 class Project < ActiveRecord::Base | |||
|
423 | 425 | end |
|
424 | 426 | end |
|
425 | 427 | |
|
426 | # Recalculates all lft and rgt values based on project names | |
|
427 | # Unlike Project.rebuild!, these values are recalculated even if the tree "looks" valid | |
|
428 | # Used in BuildProjectsTree migration | |
|
429 | def self.rebuild_tree! | |
|
430 | transaction do | |
|
431 | update_all "lft = NULL, rgt = NULL" | |
|
432 | rebuild!(false) | |
|
433 | all.each { |p| p.set_or_update_position_under(p.parent) } | |
|
434 | end | |
|
435 | end | |
|
436 | ||
|
437 | 428 | # Returns an array of the trackers used by the project and its active sub projects |
|
438 | 429 | def rolled_up_trackers |
|
439 | 430 | @rolled_up_trackers ||= |
@@ -781,11 +772,6 class Project < ActiveRecord::Base | |||
|
781 | 772 | |
|
782 | 773 | private |
|
783 | 774 | |
|
784 | def after_parent_changed(parent_was) | |
|
785 | remove_inherited_member_roles | |
|
786 | add_inherited_member_roles | |
|
787 | end | |
|
788 | ||
|
789 | 775 | def update_inherited_members |
|
790 | 776 | if parent |
|
791 | 777 | if inherit_members? && !inherit_members_was |
@@ -816,6 +802,7 class Project < ActiveRecord::Base | |||
|
816 | 802 | end |
|
817 | 803 | member.save! |
|
818 | 804 | end |
|
805 | memberships.reset | |
|
819 | 806 | end |
|
820 | 807 | end |
|
821 | 808 | |
@@ -1043,34 +1030,4 class Project < ActiveRecord::Base | |||
|
1043 | 1030 | end |
|
1044 | 1031 | update_attribute :status, STATUS_ARCHIVED |
|
1045 | 1032 | end |
|
1046 | ||
|
1047 | def update_position_under_parent | |
|
1048 | set_or_update_position_under(parent) | |
|
1049 | end | |
|
1050 | ||
|
1051 | public | |
|
1052 | ||
|
1053 | # Inserts/moves the project so that target's children or root projects stay alphabetically sorted | |
|
1054 | def set_or_update_position_under(target_parent) | |
|
1055 | parent_was = parent | |
|
1056 | sibs = (target_parent.nil? ? self.class.roots : target_parent.children) | |
|
1057 | to_be_inserted_before = sibs.sort_by {|c| c.name.to_s.downcase}.detect {|c| c.name.to_s.downcase > name.to_s.downcase } | |
|
1058 | ||
|
1059 | if to_be_inserted_before | |
|
1060 | move_to_left_of(to_be_inserted_before) | |
|
1061 | elsif target_parent.nil? | |
|
1062 | if sibs.empty? | |
|
1063 | # move_to_root adds the project in first (ie. left) position | |
|
1064 | move_to_root | |
|
1065 | else | |
|
1066 | move_to_right_of(sibs.last) unless self == sibs.last | |
|
1067 | end | |
|
1068 | else | |
|
1069 | # move_to_child_of adds the project in last (ie.right) position | |
|
1070 | move_to_child_of(target_parent) | |
|
1071 | end | |
|
1072 | if parent_was != target_parent | |
|
1073 | after_parent_changed(parent_was) | |
|
1074 | end | |
|
1075 | end | |
|
1076 | 1033 | end |
@@ -193,16 +193,3 if Rails::VERSION::MAJOR < 4 && RUBY_VERSION >= "2.1" | |||
|
193 | 193 | end |
|
194 | 194 | end |
|
195 | 195 | end |
|
196 | ||
|
197 | module CollectiveIdea | |
|
198 | module Acts | |
|
199 | module NestedSet | |
|
200 | module Model | |
|
201 | def leaf_with_new_record? | |
|
202 | new_record? || leaf_without_new_record? | |
|
203 | end | |
|
204 | alias_method_chain :leaf?, :new_record | |
|
205 | end | |
|
206 | end | |
|
207 | end | |
|
208 | end |
@@ -30,7 +30,6 require Rails.root.join('test', 'mocks', 'open_id_authentication_mock.rb').to_s | |||
|
30 | 30 | require File.expand_path(File.dirname(__FILE__) + '/object_helpers') |
|
31 | 31 | include ObjectHelpers |
|
32 | 32 | |
|
33 | require 'awesome_nested_set/version' | |
|
34 | 33 | require 'net/ldap' |
|
35 | 34 | |
|
36 | 35 | class ActionView::TestCase |
@@ -214,15 +213,9 class ActiveSupport::TestCase | |||
|
214 | 213 | mail.parts.first.body.encoded |
|
215 | 214 | end |
|
216 | 215 | |
|
217 | # awesome_nested_set new node lft and rgt value changed this refactor revision. | |
|
218 | # https://github.com/collectiveidea/awesome_nested_set/commit/199fca9bb938e40200cd90714dc69247ef017c61 | |
|
219 | # The reason of behavior change is that "self.class.base_class.unscoped" was added to this line. | |
|
220 | # https://github.com/collectiveidea/awesome_nested_set/commit/199fca9bb9#diff-f61b59a5e6319024e211b0ffdd0e4ef1R273 | |
|
221 | # It seems correct behavior because of this line comment. | |
|
222 | # https://github.com/collectiveidea/awesome_nested_set/blame/199fca9bb9/lib/awesome_nested_set/model.rb#L278 | |
|
216 | # Returns the lft value for a new root issue | |
|
223 | 217 | def new_issue_lft |
|
224 | # ::AwesomeNestedSet::VERSION > "2.1.6" ? Issue.maximum(:rgt) + 1 : 1 | |
|
225 | Issue.maximum(:rgt) + 1 | |
|
218 | 1 | |
|
226 | 219 | end |
|
227 | 220 | end |
|
228 | 221 |
General Comments 0
You need to be logged in to leave comments.
Login now