##// END OF EJS Templates
move awesome_nested_set leaf? modification to config/initializers/10-patches.rb...
Toshi MARUYAMA -
r12407:94e3eb2b8b2d
parent child
Show More
@@ -1,204 +1,217
1 require 'active_record'
1 require 'active_record'
2
2
3 module ActiveRecord
3 module ActiveRecord
4 class Base
4 class Base
5 include Redmine::I18n
5 include Redmine::I18n
6 # Translate attribute names for validation errors display
6 # Translate attribute names for validation errors display
7 def self.human_attribute_name(attr, *args)
7 def self.human_attribute_name(attr, *args)
8 attr = attr.to_s.sub(/_id$/, '')
8 attr = attr.to_s.sub(/_id$/, '')
9
9
10 l("field_#{name.underscore.gsub('/', '_')}_#{attr}", :default => ["field_#{attr}".to_sym, attr])
10 l("field_#{name.underscore.gsub('/', '_')}_#{attr}", :default => ["field_#{attr}".to_sym, attr])
11 end
11 end
12 end
12 end
13
13
14 # Undefines private Kernel#open method to allow using `open` scopes in models.
14 # Undefines private Kernel#open method to allow using `open` scopes in models.
15 # See Defect #11545 (http://www.redmine.org/issues/11545) for details.
15 # See Defect #11545 (http://www.redmine.org/issues/11545) for details.
16 class Base
16 class Base
17 class << self
17 class << self
18 undef open
18 undef open
19 end
19 end
20 end
20 end
21 class Relation ; undef open ; end
21 class Relation ; undef open ; end
22 end
22 end
23
23
24 module ActionView
24 module ActionView
25 module Helpers
25 module Helpers
26 module DateHelper
26 module DateHelper
27 # distance_of_time_in_words breaks when difference is greater than 30 years
27 # distance_of_time_in_words breaks when difference is greater than 30 years
28 def distance_of_date_in_words(from_date, to_date = 0, options = {})
28 def distance_of_date_in_words(from_date, to_date = 0, options = {})
29 from_date = from_date.to_date if from_date.respond_to?(:to_date)
29 from_date = from_date.to_date if from_date.respond_to?(:to_date)
30 to_date = to_date.to_date if to_date.respond_to?(:to_date)
30 to_date = to_date.to_date if to_date.respond_to?(:to_date)
31 distance_in_days = (to_date - from_date).abs
31 distance_in_days = (to_date - from_date).abs
32
32
33 I18n.with_options :locale => options[:locale], :scope => :'datetime.distance_in_words' do |locale|
33 I18n.with_options :locale => options[:locale], :scope => :'datetime.distance_in_words' do |locale|
34 case distance_in_days
34 case distance_in_days
35 when 0..60 then locale.t :x_days, :count => distance_in_days.round
35 when 0..60 then locale.t :x_days, :count => distance_in_days.round
36 when 61..720 then locale.t :about_x_months, :count => (distance_in_days / 30).round
36 when 61..720 then locale.t :about_x_months, :count => (distance_in_days / 30).round
37 else locale.t :over_x_years, :count => (distance_in_days / 365).floor
37 else locale.t :over_x_years, :count => (distance_in_days / 365).floor
38 end
38 end
39 end
39 end
40 end
40 end
41 end
41 end
42 end
42 end
43
43
44 class Resolver
44 class Resolver
45 def find_all(name, prefix=nil, partial=false, details={}, key=nil, locals=[])
45 def find_all(name, prefix=nil, partial=false, details={}, key=nil, locals=[])
46 cached(key, [name, prefix, partial], details, locals) do
46 cached(key, [name, prefix, partial], details, locals) do
47 if details[:formats] & [:xml, :json]
47 if details[:formats] & [:xml, :json]
48 details = details.dup
48 details = details.dup
49 details[:formats] = details[:formats].dup + [:api]
49 details[:formats] = details[:formats].dup + [:api]
50 end
50 end
51 find_templates(name, prefix, partial, details)
51 find_templates(name, prefix, partial, details)
52 end
52 end
53 end
53 end
54 end
54 end
55 end
55 end
56
56
57 # Do not HTML escape text templates
57 # Do not HTML escape text templates
58 module ActionView
58 module ActionView
59 class Template
59 class Template
60 module Handlers
60 module Handlers
61 class ERB
61 class ERB
62 def call(template)
62 def call(template)
63 if template.source.encoding_aware?
63 if template.source.encoding_aware?
64 # First, convert to BINARY, so in case the encoding is
64 # First, convert to BINARY, so in case the encoding is
65 # wrong, we can still find an encoding tag
65 # wrong, we can still find an encoding tag
66 # (<%# encoding %>) inside the String using a regular
66 # (<%# encoding %>) inside the String using a regular
67 # expression
67 # expression
68 template_source = template.source.dup.force_encoding("BINARY")
68 template_source = template.source.dup.force_encoding("BINARY")
69
69
70 erb = template_source.gsub(ENCODING_TAG, '')
70 erb = template_source.gsub(ENCODING_TAG, '')
71 encoding = $2
71 encoding = $2
72
72
73 erb.force_encoding valid_encoding(template.source.dup, encoding)
73 erb.force_encoding valid_encoding(template.source.dup, encoding)
74
74
75 # Always make sure we return a String in the default_internal
75 # Always make sure we return a String in the default_internal
76 erb.encode!
76 erb.encode!
77 else
77 else
78 erb = template.source.dup
78 erb = template.source.dup
79 end
79 end
80
80
81 self.class.erb_implementation.new(
81 self.class.erb_implementation.new(
82 erb,
82 erb,
83 :trim => (self.class.erb_trim_mode == "-"),
83 :trim => (self.class.erb_trim_mode == "-"),
84 :escape => template.identifier =~ /\.text/ # only escape HTML templates
84 :escape => template.identifier =~ /\.text/ # only escape HTML templates
85 ).src
85 ).src
86 end
86 end
87 end
87 end
88 end
88 end
89 end
89 end
90 end
90 end
91
91
92 ActionView::Base.field_error_proc = Proc.new{ |html_tag, instance| html_tag || ''.html_safe }
92 ActionView::Base.field_error_proc = Proc.new{ |html_tag, instance| html_tag || ''.html_safe }
93
93
94 # HTML5: <option value=""></option> is invalid, use <option value="">&nbsp;</option> instead
94 # HTML5: <option value=""></option> is invalid, use <option value="">&nbsp;</option> instead
95 module ActionView
95 module ActionView
96 module Helpers
96 module Helpers
97 class InstanceTag
97 class InstanceTag
98 private
98 private
99 def add_options_with_non_empty_blank_option(option_tags, options, value = nil)
99 def add_options_with_non_empty_blank_option(option_tags, options, value = nil)
100 if options[:include_blank] == true
100 if options[:include_blank] == true
101 options = options.dup
101 options = options.dup
102 options[:include_blank] = '&nbsp;'.html_safe
102 options[:include_blank] = '&nbsp;'.html_safe
103 end
103 end
104 add_options_without_non_empty_blank_option(option_tags, options, value)
104 add_options_without_non_empty_blank_option(option_tags, options, value)
105 end
105 end
106 alias_method_chain :add_options, :non_empty_blank_option
106 alias_method_chain :add_options, :non_empty_blank_option
107 end
107 end
108
108
109 module FormTagHelper
109 module FormTagHelper
110 def select_tag_with_non_empty_blank_option(name, option_tags = nil, options = {})
110 def select_tag_with_non_empty_blank_option(name, option_tags = nil, options = {})
111 if options.delete(:include_blank)
111 if options.delete(:include_blank)
112 options[:prompt] = '&nbsp;'.html_safe
112 options[:prompt] = '&nbsp;'.html_safe
113 end
113 end
114 select_tag_without_non_empty_blank_option(name, option_tags, options)
114 select_tag_without_non_empty_blank_option(name, option_tags, options)
115 end
115 end
116 alias_method_chain :select_tag, :non_empty_blank_option
116 alias_method_chain :select_tag, :non_empty_blank_option
117 end
117 end
118
118
119 module FormOptionsHelper
119 module FormOptionsHelper
120 def options_for_select_with_non_empty_blank_option(container, selected = nil)
120 def options_for_select_with_non_empty_blank_option(container, selected = nil)
121 if container.is_a?(Array)
121 if container.is_a?(Array)
122 container = container.map {|element| element.blank? ? ["&nbsp;".html_safe, ""] : element}
122 container = container.map {|element| element.blank? ? ["&nbsp;".html_safe, ""] : element}
123 end
123 end
124 options_for_select_without_non_empty_blank_option(container, selected)
124 options_for_select_without_non_empty_blank_option(container, selected)
125 end
125 end
126 alias_method_chain :options_for_select, :non_empty_blank_option
126 alias_method_chain :options_for_select, :non_empty_blank_option
127 end
127 end
128 end
128 end
129 end
129 end
130
130
131 require 'mail'
131 require 'mail'
132
132
133 module DeliveryMethods
133 module DeliveryMethods
134 class AsyncSMTP < ::Mail::SMTP
134 class AsyncSMTP < ::Mail::SMTP
135 def deliver!(*args)
135 def deliver!(*args)
136 Thread.start do
136 Thread.start do
137 super *args
137 super *args
138 end
138 end
139 end
139 end
140 end
140 end
141
141
142 class AsyncSendmail < ::Mail::Sendmail
142 class AsyncSendmail < ::Mail::Sendmail
143 def deliver!(*args)
143 def deliver!(*args)
144 Thread.start do
144 Thread.start do
145 super *args
145 super *args
146 end
146 end
147 end
147 end
148 end
148 end
149
149
150 class TmpFile
150 class TmpFile
151 def initialize(*args); end
151 def initialize(*args); end
152
152
153 def deliver!(mail)
153 def deliver!(mail)
154 dest_dir = File.join(Rails.root, 'tmp', 'emails')
154 dest_dir = File.join(Rails.root, 'tmp', 'emails')
155 Dir.mkdir(dest_dir) unless File.directory?(dest_dir)
155 Dir.mkdir(dest_dir) unless File.directory?(dest_dir)
156 File.open(File.join(dest_dir, mail.message_id.gsub(/[<>]/, '') + '.eml'), 'wb') {|f| f.write(mail.encoded) }
156 File.open(File.join(dest_dir, mail.message_id.gsub(/[<>]/, '') + '.eml'), 'wb') {|f| f.write(mail.encoded) }
157 end
157 end
158 end
158 end
159 end
159 end
160
160
161 ActionMailer::Base.add_delivery_method :async_smtp, DeliveryMethods::AsyncSMTP
161 ActionMailer::Base.add_delivery_method :async_smtp, DeliveryMethods::AsyncSMTP
162 ActionMailer::Base.add_delivery_method :async_sendmail, DeliveryMethods::AsyncSendmail
162 ActionMailer::Base.add_delivery_method :async_sendmail, DeliveryMethods::AsyncSendmail
163 ActionMailer::Base.add_delivery_method :tmp_file, DeliveryMethods::TmpFile
163 ActionMailer::Base.add_delivery_method :tmp_file, DeliveryMethods::TmpFile
164
164
165 # Changes how sent emails are logged
165 # Changes how sent emails are logged
166 # Rails doesn't log cc and bcc which is misleading when using bcc only (#12090)
166 # Rails doesn't log cc and bcc which is misleading when using bcc only (#12090)
167 module ActionMailer
167 module ActionMailer
168 class LogSubscriber < ActiveSupport::LogSubscriber
168 class LogSubscriber < ActiveSupport::LogSubscriber
169 def deliver(event)
169 def deliver(event)
170 recipients = [:to, :cc, :bcc].inject("") do |s, header|
170 recipients = [:to, :cc, :bcc].inject("") do |s, header|
171 r = Array.wrap(event.payload[header])
171 r = Array.wrap(event.payload[header])
172 if r.any?
172 if r.any?
173 s << "\n #{header}: #{r.join(', ')}"
173 s << "\n #{header}: #{r.join(', ')}"
174 end
174 end
175 s
175 s
176 end
176 end
177 info("\nSent email \"#{event.payload[:subject]}\" (%1.fms)#{recipients}" % event.duration)
177 info("\nSent email \"#{event.payload[:subject]}\" (%1.fms)#{recipients}" % event.duration)
178 debug(event.payload[:mail])
178 debug(event.payload[:mail])
179 end
179 end
180 end
180 end
181 end
181 end
182
182
183 module ActionController
183 module ActionController
184 module MimeResponds
184 module MimeResponds
185 class Collector
185 class Collector
186 def api(&block)
186 def api(&block)
187 any(:xml, :json, &block)
187 any(:xml, :json, &block)
188 end
188 end
189 end
189 end
190 end
190 end
191 end
191 end
192
192
193 module ActionController
193 module ActionController
194 class Base
194 class Base
195 # Displays an explicit message instead of a NoMethodError exception
195 # Displays an explicit message instead of a NoMethodError exception
196 # when trying to start Redmine with an old session_store.rb
196 # when trying to start Redmine with an old session_store.rb
197 # TODO: remove it in a later version
197 # TODO: remove it in a later version
198 def self.session=(*args)
198 def self.session=(*args)
199 $stderr.puts "Please remove config/initializers/session_store.rb and run `rake generate_secret_token`.\n" +
199 $stderr.puts "Please remove config/initializers/session_store.rb and run `rake generate_secret_token`.\n" +
200 "Setting the session secret with ActionController.session= is no longer supported in Rails 3."
200 "Setting the session secret with ActionController.session= is no longer supported in Rails 3."
201 exit 1
201 exit 1
202 end
202 end
203 end
203 end
204 end
204 end
205
206 module CollectiveIdea
207 module Acts
208 module NestedSet
209 module Model
210 def leaf_with_new_record?
211 new_record? || leaf_without_new_record?
212 end
213 alias_method_chain :leaf?, :new_record
214 end
215 end
216 end
217 end
@@ -1,748 +1,748
1 module CollectiveIdea #:nodoc:
1 module CollectiveIdea #:nodoc:
2 module Acts #:nodoc:
2 module Acts #:nodoc:
3 module NestedSet #:nodoc:
3 module NestedSet #:nodoc:
4
4
5 # This acts provides Nested Set functionality. Nested Set is a smart way to implement
5 # This acts provides Nested Set functionality. Nested Set is a smart way to implement
6 # an _ordered_ tree, with the added feature that you can select the children and all of their
6 # an _ordered_ tree, with the added feature that you can select the children and all of their
7 # descendants with a single query. The drawback is that insertion or move need some complex
7 # descendants with a single query. The drawback is that insertion or move need some complex
8 # sql queries. But everything is done here by this module!
8 # sql queries. But everything is done here by this module!
9 #
9 #
10 # Nested sets are appropriate each time you want either an orderd tree (menus,
10 # Nested sets are appropriate each time you want either an orderd tree (menus,
11 # commercial categories) or an efficient way of querying big trees (threaded posts).
11 # commercial categories) or an efficient way of querying big trees (threaded posts).
12 #
12 #
13 # == API
13 # == API
14 #
14 #
15 # Methods names are aligned with acts_as_tree as much as possible to make replacment from one
15 # Methods names are aligned with acts_as_tree as much as possible to make replacment from one
16 # by another easier.
16 # by another easier.
17 #
17 #
18 # item.children.create(:name => "child1")
18 # item.children.create(:name => "child1")
19 #
19 #
20
20
21 # Configuration options are:
21 # Configuration options are:
22 #
22 #
23 # * +:parent_column+ - specifies the column name to use for keeping the position integer (default: parent_id)
23 # * +:parent_column+ - specifies the column name to use for keeping the position integer (default: parent_id)
24 # * +:left_column+ - column name for left boundry data, default "lft"
24 # * +:left_column+ - column name for left boundry data, default "lft"
25 # * +:right_column+ - column name for right boundry data, default "rgt"
25 # * +:right_column+ - column name for right boundry data, default "rgt"
26 # * +:depth_column+ - column name for the depth data, default "depth"
26 # * +:depth_column+ - column name for the depth data, default "depth"
27 # * +:scope+ - restricts what is to be considered a list. Given a symbol, it'll attach "_id"
27 # * +:scope+ - restricts what is to be considered a list. Given a symbol, it'll attach "_id"
28 # (if it hasn't been already) and use that as the foreign key restriction. You
28 # (if it hasn't been already) and use that as the foreign key restriction. You
29 # can also pass an array to scope by multiple attributes.
29 # can also pass an array to scope by multiple attributes.
30 # Example: <tt>acts_as_nested_set :scope => [:notable_id, :notable_type]</tt>
30 # Example: <tt>acts_as_nested_set :scope => [:notable_id, :notable_type]</tt>
31 # * +:dependent+ - behavior for cascading destroy. If set to :destroy, all the
31 # * +:dependent+ - behavior for cascading destroy. If set to :destroy, all the
32 # child objects are destroyed alongside this object by calling their destroy
32 # child objects are destroyed alongside this object by calling their destroy
33 # method. If set to :delete_all (default), all the child objects are deleted
33 # method. If set to :delete_all (default), all the child objects are deleted
34 # without calling their destroy method.
34 # without calling their destroy method.
35 # * +:counter_cache+ adds a counter cache for the number of children.
35 # * +:counter_cache+ adds a counter cache for the number of children.
36 # defaults to false.
36 # defaults to false.
37 # Example: <tt>acts_as_nested_set :counter_cache => :children_count</tt>
37 # Example: <tt>acts_as_nested_set :counter_cache => :children_count</tt>
38 # * +:order_column+ on which column to do sorting, by default it is the left_column_name
38 # * +:order_column+ on which column to do sorting, by default it is the left_column_name
39 # Example: <tt>acts_as_nested_set :order_column => :position</tt>
39 # Example: <tt>acts_as_nested_set :order_column => :position</tt>
40 #
40 #
41 # See CollectiveIdea::Acts::NestedSet::Model::ClassMethods for a list of class methods and
41 # See CollectiveIdea::Acts::NestedSet::Model::ClassMethods for a list of class methods and
42 # CollectiveIdea::Acts::NestedSet::Model for a list of instance methods added
42 # CollectiveIdea::Acts::NestedSet::Model for a list of instance methods added
43 # to acts_as_nested_set models
43 # to acts_as_nested_set models
44 def acts_as_nested_set(options = {})
44 def acts_as_nested_set(options = {})
45 options = {
45 options = {
46 :parent_column => 'parent_id',
46 :parent_column => 'parent_id',
47 :left_column => 'lft',
47 :left_column => 'lft',
48 :right_column => 'rgt',
48 :right_column => 'rgt',
49 :depth_column => 'depth',
49 :depth_column => 'depth',
50 :dependent => :delete_all, # or :destroy
50 :dependent => :delete_all, # or :destroy
51 :polymorphic => false,
51 :polymorphic => false,
52 :counter_cache => false
52 :counter_cache => false
53 }.merge(options)
53 }.merge(options)
54
54
55 if options[:scope].is_a?(Symbol) && options[:scope].to_s !~ /_id$/
55 if options[:scope].is_a?(Symbol) && options[:scope].to_s !~ /_id$/
56 options[:scope] = "#{options[:scope]}_id".intern
56 options[:scope] = "#{options[:scope]}_id".intern
57 end
57 end
58
58
59 class_attribute :acts_as_nested_set_options
59 class_attribute :acts_as_nested_set_options
60 self.acts_as_nested_set_options = options
60 self.acts_as_nested_set_options = options
61
61
62 include CollectiveIdea::Acts::NestedSet::Model
62 include CollectiveIdea::Acts::NestedSet::Model
63 include Columns
63 include Columns
64 extend Columns
64 extend Columns
65
65
66 belongs_to :parent, :class_name => self.base_class.to_s,
66 belongs_to :parent, :class_name => self.base_class.to_s,
67 :foreign_key => parent_column_name,
67 :foreign_key => parent_column_name,
68 :counter_cache => options[:counter_cache],
68 :counter_cache => options[:counter_cache],
69 :inverse_of => (:children unless options[:polymorphic]),
69 :inverse_of => (:children unless options[:polymorphic]),
70 :polymorphic => options[:polymorphic]
70 :polymorphic => options[:polymorphic]
71
71
72 has_many_children_options = {
72 has_many_children_options = {
73 :class_name => self.base_class.to_s,
73 :class_name => self.base_class.to_s,
74 :foreign_key => parent_column_name,
74 :foreign_key => parent_column_name,
75 :order => order_column,
75 :order => order_column,
76 :inverse_of => (:parent unless options[:polymorphic]),
76 :inverse_of => (:parent unless options[:polymorphic]),
77 }
77 }
78
78
79 # Add callbacks, if they were supplied.. otherwise, we don't want them.
79 # Add callbacks, if they were supplied.. otherwise, we don't want them.
80 [:before_add, :after_add, :before_remove, :after_remove].each do |ar_callback|
80 [:before_add, :after_add, :before_remove, :after_remove].each do |ar_callback|
81 has_many_children_options.update(ar_callback => options[ar_callback]) if options[ar_callback]
81 has_many_children_options.update(ar_callback => options[ar_callback]) if options[ar_callback]
82 end
82 end
83
83
84 has_many :children, has_many_children_options
84 has_many :children, has_many_children_options
85
85
86 attr_accessor :skip_before_destroy
86 attr_accessor :skip_before_destroy
87
87
88 before_create :set_default_left_and_right
88 before_create :set_default_left_and_right
89 before_save :store_new_parent
89 before_save :store_new_parent
90 after_save :move_to_new_parent, :set_depth!
90 after_save :move_to_new_parent, :set_depth!
91 before_destroy :destroy_descendants
91 before_destroy :destroy_descendants
92
92
93 # no assignment to structure fields
93 # no assignment to structure fields
94 [left_column_name, right_column_name, depth_column_name].each do |column|
94 [left_column_name, right_column_name, depth_column_name].each do |column|
95 module_eval <<-"end_eval", __FILE__, __LINE__
95 module_eval <<-"end_eval", __FILE__, __LINE__
96 def #{column}=(x)
96 def #{column}=(x)
97 raise ActiveRecord::ActiveRecordError, "Unauthorized assignment to #{column}: it's an internal field handled by acts_as_nested_set code, use move_to_* methods instead."
97 raise ActiveRecord::ActiveRecordError, "Unauthorized assignment to #{column}: it's an internal field handled by acts_as_nested_set code, use move_to_* methods instead."
98 end
98 end
99 end_eval
99 end_eval
100 end
100 end
101
101
102 define_model_callbacks :move
102 define_model_callbacks :move
103 end
103 end
104
104
105 module Model
105 module Model
106 extend ActiveSupport::Concern
106 extend ActiveSupport::Concern
107
107
108 included do
108 included do
109 delegate :quoted_table_name, :to => self
109 delegate :quoted_table_name, :to => self
110 end
110 end
111
111
112 module ClassMethods
112 module ClassMethods
113 # Returns the first root
113 # Returns the first root
114 def root
114 def root
115 roots.first
115 roots.first
116 end
116 end
117
117
118 def roots
118 def roots
119 where(parent_column_name => nil).order(quoted_left_column_full_name)
119 where(parent_column_name => nil).order(quoted_left_column_full_name)
120 end
120 end
121
121
122 def leaves
122 def leaves
123 where("#{quoted_right_column_full_name} - #{quoted_left_column_full_name} = 1").order(quoted_left_column_full_name)
123 where("#{quoted_right_column_full_name} - #{quoted_left_column_full_name} = 1").order(quoted_left_column_full_name)
124 end
124 end
125
125
126 def valid?
126 def valid?
127 left_and_rights_valid? && no_duplicates_for_columns? && all_roots_valid?
127 left_and_rights_valid? && no_duplicates_for_columns? && all_roots_valid?
128 end
128 end
129
129
130 def left_and_rights_valid?
130 def left_and_rights_valid?
131 ## AS clause not supported in Oracle in FROM clause for aliasing table name
131 ## AS clause not supported in Oracle in FROM clause for aliasing table name
132 joins("LEFT OUTER JOIN #{quoted_table_name}" +
132 joins("LEFT OUTER JOIN #{quoted_table_name}" +
133 (connection.adapter_name.match(/Oracle/).nil? ? " AS " : " ") +
133 (connection.adapter_name.match(/Oracle/).nil? ? " AS " : " ") +
134 "parent ON " +
134 "parent ON " +
135 "#{quoted_parent_column_full_name} = parent.#{primary_key}").
135 "#{quoted_parent_column_full_name} = parent.#{primary_key}").
136 where(
136 where(
137 "#{quoted_left_column_full_name} IS NULL OR " +
137 "#{quoted_left_column_full_name} IS NULL OR " +
138 "#{quoted_right_column_full_name} IS NULL OR " +
138 "#{quoted_right_column_full_name} IS NULL OR " +
139 "#{quoted_left_column_full_name} >= " +
139 "#{quoted_left_column_full_name} >= " +
140 "#{quoted_right_column_full_name} OR " +
140 "#{quoted_right_column_full_name} OR " +
141 "(#{quoted_parent_column_full_name} IS NOT NULL AND " +
141 "(#{quoted_parent_column_full_name} IS NOT NULL AND " +
142 "(#{quoted_left_column_full_name} <= parent.#{quoted_left_column_name} OR " +
142 "(#{quoted_left_column_full_name} <= parent.#{quoted_left_column_name} OR " +
143 "#{quoted_right_column_full_name} >= parent.#{quoted_right_column_name}))"
143 "#{quoted_right_column_full_name} >= parent.#{quoted_right_column_name}))"
144 ).count == 0
144 ).count == 0
145 end
145 end
146
146
147 def no_duplicates_for_columns?
147 def no_duplicates_for_columns?
148 scope_string = Array(acts_as_nested_set_options[:scope]).map do |c|
148 scope_string = Array(acts_as_nested_set_options[:scope]).map do |c|
149 connection.quote_column_name(c)
149 connection.quote_column_name(c)
150 end.push(nil).join(", ")
150 end.push(nil).join(", ")
151 [quoted_left_column_full_name, quoted_right_column_full_name].all? do |column|
151 [quoted_left_column_full_name, quoted_right_column_full_name].all? do |column|
152 # No duplicates
152 # No duplicates
153 select("#{scope_string}#{column}, COUNT(#{column})").
153 select("#{scope_string}#{column}, COUNT(#{column})").
154 group("#{scope_string}#{column}").
154 group("#{scope_string}#{column}").
155 having("COUNT(#{column}) > 1").
155 having("COUNT(#{column}) > 1").
156 first.nil?
156 first.nil?
157 end
157 end
158 end
158 end
159
159
160 # Wrapper for each_root_valid? that can deal with scope.
160 # Wrapper for each_root_valid? that can deal with scope.
161 def all_roots_valid?
161 def all_roots_valid?
162 if acts_as_nested_set_options[:scope]
162 if acts_as_nested_set_options[:scope]
163 roots.group_by {|record| scope_column_names.collect {|col| record.send(col.to_sym) } }.all? do |scope, grouped_roots|
163 roots.group_by {|record| scope_column_names.collect {|col| record.send(col.to_sym) } }.all? do |scope, grouped_roots|
164 each_root_valid?(grouped_roots)
164 each_root_valid?(grouped_roots)
165 end
165 end
166 else
166 else
167 each_root_valid?(roots)
167 each_root_valid?(roots)
168 end
168 end
169 end
169 end
170
170
171 def each_root_valid?(roots_to_validate)
171 def each_root_valid?(roots_to_validate)
172 left = right = 0
172 left = right = 0
173 roots_to_validate.all? do |root|
173 roots_to_validate.all? do |root|
174 (root.left > left && root.right > right).tap do
174 (root.left > left && root.right > right).tap do
175 left = root.left
175 left = root.left
176 right = root.right
176 right = root.right
177 end
177 end
178 end
178 end
179 end
179 end
180
180
181 # Rebuilds the left & rights if unset or invalid.
181 # Rebuilds the left & rights if unset or invalid.
182 # Also very useful for converting from acts_as_tree.
182 # Also very useful for converting from acts_as_tree.
183 def rebuild!(validate_nodes = true)
183 def rebuild!(validate_nodes = true)
184 # Don't rebuild a valid tree.
184 # Don't rebuild a valid tree.
185 return true if valid?
185 return true if valid?
186
186
187 scope = lambda{|node|}
187 scope = lambda{|node|}
188 if acts_as_nested_set_options[:scope]
188 if acts_as_nested_set_options[:scope]
189 scope = lambda{|node|
189 scope = lambda{|node|
190 scope_column_names.inject(""){|str, column_name|
190 scope_column_names.inject(""){|str, column_name|
191 str << "AND #{connection.quote_column_name(column_name)} = #{connection.quote(node.send(column_name.to_sym))} "
191 str << "AND #{connection.quote_column_name(column_name)} = #{connection.quote(node.send(column_name.to_sym))} "
192 }
192 }
193 }
193 }
194 end
194 end
195 indices = {}
195 indices = {}
196
196
197 set_left_and_rights = lambda do |node|
197 set_left_and_rights = lambda do |node|
198 # set left
198 # set left
199 node[left_column_name] = indices[scope.call(node)] += 1
199 node[left_column_name] = indices[scope.call(node)] += 1
200 # find
200 # find
201 where(["#{quoted_parent_column_name} = ? #{scope.call(node)}", node]).
201 where(["#{quoted_parent_column_name} = ? #{scope.call(node)}", node]).
202 order(acts_as_nested_set_options[:order]).
202 order(acts_as_nested_set_options[:order]).
203 each{|n| set_left_and_rights.call(n) }
203 each{|n| set_left_and_rights.call(n) }
204 # set right
204 # set right
205 node[right_column_name] = indices[scope.call(node)] += 1
205 node[right_column_name] = indices[scope.call(node)] += 1
206 node.save!(:validate => validate_nodes)
206 node.save!(:validate => validate_nodes)
207 end
207 end
208
208
209 # Find root node(s)
209 # Find root node(s)
210 root_nodes = where("#{quoted_parent_column_name} IS NULL").
210 root_nodes = where("#{quoted_parent_column_name} IS NULL").
211 order(acts_as_nested_set_options[:order]).each do |root_node|
211 order(acts_as_nested_set_options[:order]).each do |root_node|
212 # setup index for this scope
212 # setup index for this scope
213 indices[scope.call(root_node)] ||= 0
213 indices[scope.call(root_node)] ||= 0
214 set_left_and_rights.call(root_node)
214 set_left_and_rights.call(root_node)
215 end
215 end
216 end
216 end
217
217
218 # Iterates over tree elements and determines the current level in the tree.
218 # Iterates over tree elements and determines the current level in the tree.
219 # Only accepts default ordering, odering by an other column than lft
219 # Only accepts default ordering, odering by an other column than lft
220 # does not work. This method is much more efficent than calling level
220 # does not work. This method is much more efficent than calling level
221 # because it doesn't require any additional database queries.
221 # because it doesn't require any additional database queries.
222 #
222 #
223 # Example:
223 # Example:
224 # Category.each_with_level(Category.root.self_and_descendants) do |o, level|
224 # Category.each_with_level(Category.root.self_and_descendants) do |o, level|
225 #
225 #
226 def each_with_level(objects)
226 def each_with_level(objects)
227 path = [nil]
227 path = [nil]
228 objects.each do |o|
228 objects.each do |o|
229 if o.parent_id != path.last
229 if o.parent_id != path.last
230 # we are on a new level, did we descend or ascend?
230 # we are on a new level, did we descend or ascend?
231 if path.include?(o.parent_id)
231 if path.include?(o.parent_id)
232 # remove wrong wrong tailing paths elements
232 # remove wrong wrong tailing paths elements
233 path.pop while path.last != o.parent_id
233 path.pop while path.last != o.parent_id
234 else
234 else
235 path << o.parent_id
235 path << o.parent_id
236 end
236 end
237 end
237 end
238 yield(o, path.length - 1)
238 yield(o, path.length - 1)
239 end
239 end
240 end
240 end
241
241
242 # Same as each_with_level - Accepts a string as a second argument to sort the list
242 # Same as each_with_level - Accepts a string as a second argument to sort the list
243 # Example:
243 # Example:
244 # Category.each_with_level(Category.root.self_and_descendants, :sort_by_this_column) do |o, level|
244 # Category.each_with_level(Category.root.self_and_descendants, :sort_by_this_column) do |o, level|
245 def sorted_each_with_level(objects, order)
245 def sorted_each_with_level(objects, order)
246 path = [nil]
246 path = [nil]
247 children = []
247 children = []
248 objects.each do |o|
248 objects.each do |o|
249 children << o if o.leaf?
249 children << o if o.leaf?
250 if o.parent_id != path.last
250 if o.parent_id != path.last
251 if !children.empty? && !o.leaf?
251 if !children.empty? && !o.leaf?
252 children.sort_by! &order
252 children.sort_by! &order
253 children.each { |c| yield(c, path.length-1) }
253 children.each { |c| yield(c, path.length-1) }
254 children = []
254 children = []
255 end
255 end
256 # we are on a new level, did we decent or ascent?
256 # we are on a new level, did we decent or ascent?
257 if path.include?(o.parent_id)
257 if path.include?(o.parent_id)
258 # remove wrong wrong tailing paths elements
258 # remove wrong wrong tailing paths elements
259 path.pop while path.last != o.parent_id
259 path.pop while path.last != o.parent_id
260 else
260 else
261 path << o.parent_id
261 path << o.parent_id
262 end
262 end
263 end
263 end
264 yield(o,path.length-1) if !o.leaf?
264 yield(o,path.length-1) if !o.leaf?
265 end
265 end
266 if !children.empty?
266 if !children.empty?
267 children.sort_by! &order
267 children.sort_by! &order
268 children.each { |c| yield(c, path.length-1) }
268 children.each { |c| yield(c, path.length-1) }
269 end
269 end
270 end
270 end
271
271
272 def associate_parents(objects)
272 def associate_parents(objects)
273 if objects.all?{|o| o.respond_to?(:association)}
273 if objects.all?{|o| o.respond_to?(:association)}
274 id_indexed = objects.index_by(&:id)
274 id_indexed = objects.index_by(&:id)
275 objects.each do |object|
275 objects.each do |object|
276 if !(association = object.association(:parent)).loaded? && (parent = id_indexed[object.parent_id])
276 if !(association = object.association(:parent)).loaded? && (parent = id_indexed[object.parent_id])
277 association.target = parent
277 association.target = parent
278 association.set_inverse_instance(parent)
278 association.set_inverse_instance(parent)
279 end
279 end
280 end
280 end
281 else
281 else
282 objects
282 objects
283 end
283 end
284 end
284 end
285 end
285 end
286
286
287 # Any instance method that returns a collection makes use of Rails 2.1's named_scope (which is bundled for Rails 2.0), so it can be treated as a finder.
287 # Any instance method that returns a collection makes use of Rails 2.1's named_scope (which is bundled for Rails 2.0), so it can be treated as a finder.
288 #
288 #
289 # category.self_and_descendants.count
289 # category.self_and_descendants.count
290 # category.ancestors.find(:all, :conditions => "name like '%foo%'")
290 # category.ancestors.find(:all, :conditions => "name like '%foo%'")
291 # Value of the parent column
291 # Value of the parent column
292 def parent_id
292 def parent_id
293 self[parent_column_name]
293 self[parent_column_name]
294 end
294 end
295
295
296 # Value of the left column
296 # Value of the left column
297 def left
297 def left
298 self[left_column_name]
298 self[left_column_name]
299 end
299 end
300
300
301 # Value of the right column
301 # Value of the right column
302 def right
302 def right
303 self[right_column_name]
303 self[right_column_name]
304 end
304 end
305
305
306 # Returns true if this is a root node.
306 # Returns true if this is a root node.
307 def root?
307 def root?
308 parent_id.nil?
308 parent_id.nil?
309 end
309 end
310
310
311 # Returns true if this is the end of a branch.
311 # Returns true if this is the end of a branch.
312 def leaf?
312 def leaf?
313 new_record? || (persisted? && right.to_i - left.to_i == 1)
313 persisted? && right.to_i - left.to_i == 1
314 end
314 end
315
315
316 # Returns true is this is a child node
316 # Returns true is this is a child node
317 def child?
317 def child?
318 !root?
318 !root?
319 end
319 end
320
320
321 # Returns root
321 # Returns root
322 def root
322 def root
323 if persisted?
323 if persisted?
324 self_and_ancestors.where(parent_column_name => nil).first
324 self_and_ancestors.where(parent_column_name => nil).first
325 else
325 else
326 if parent_id && current_parent = nested_set_scope.find(parent_id)
326 if parent_id && current_parent = nested_set_scope.find(parent_id)
327 current_parent.root
327 current_parent.root
328 else
328 else
329 self
329 self
330 end
330 end
331 end
331 end
332 end
332 end
333
333
334 # Returns the array of all parents and self
334 # Returns the array of all parents and self
335 def self_and_ancestors
335 def self_and_ancestors
336 nested_set_scope.where([
336 nested_set_scope.where([
337 "#{quoted_left_column_full_name} <= ? AND #{quoted_right_column_full_name} >= ?", left, right
337 "#{quoted_left_column_full_name} <= ? AND #{quoted_right_column_full_name} >= ?", left, right
338 ])
338 ])
339 end
339 end
340
340
341 # Returns an array of all parents
341 # Returns an array of all parents
342 def ancestors
342 def ancestors
343 without_self self_and_ancestors
343 without_self self_and_ancestors
344 end
344 end
345
345
346 # Returns the array of all children of the parent, including self
346 # Returns the array of all children of the parent, including self
347 def self_and_siblings
347 def self_and_siblings
348 nested_set_scope.where(parent_column_name => parent_id)
348 nested_set_scope.where(parent_column_name => parent_id)
349 end
349 end
350
350
351 # Returns the array of all children of the parent, except self
351 # Returns the array of all children of the parent, except self
352 def siblings
352 def siblings
353 without_self self_and_siblings
353 without_self self_and_siblings
354 end
354 end
355
355
356 # Returns a set of all of its nested children which do not have children
356 # Returns a set of all of its nested children which do not have children
357 def leaves
357 def leaves
358 descendants.where("#{quoted_right_column_full_name} - #{quoted_left_column_full_name} = 1")
358 descendants.where("#{quoted_right_column_full_name} - #{quoted_left_column_full_name} = 1")
359 end
359 end
360
360
361 # Returns the level of this object in the tree
361 # Returns the level of this object in the tree
362 # root level is 0
362 # root level is 0
363 def level
363 def level
364 parent_id.nil? ? 0 : compute_level
364 parent_id.nil? ? 0 : compute_level
365 end
365 end
366
366
367 # Returns a set of itself and all of its nested children
367 # Returns a set of itself and all of its nested children
368 def self_and_descendants
368 def self_and_descendants
369 nested_set_scope.where([
369 nested_set_scope.where([
370 "#{quoted_left_column_full_name} >= ? AND #{quoted_left_column_full_name} < ?", left, right
370 "#{quoted_left_column_full_name} >= ? AND #{quoted_left_column_full_name} < ?", left, right
371 # using _left_ for both sides here lets us benefit from an index on that column if one exists
371 # using _left_ for both sides here lets us benefit from an index on that column if one exists
372 ])
372 ])
373 end
373 end
374
374
375 # Returns a set of all of its children and nested children
375 # Returns a set of all of its children and nested children
376 def descendants
376 def descendants
377 without_self self_and_descendants
377 without_self self_and_descendants
378 end
378 end
379
379
380 def is_descendant_of?(other)
380 def is_descendant_of?(other)
381 other.left < self.left && self.left < other.right && same_scope?(other)
381 other.left < self.left && self.left < other.right && same_scope?(other)
382 end
382 end
383
383
384 def is_or_is_descendant_of?(other)
384 def is_or_is_descendant_of?(other)
385 other.left <= self.left && self.left < other.right && same_scope?(other)
385 other.left <= self.left && self.left < other.right && same_scope?(other)
386 end
386 end
387
387
388 def is_ancestor_of?(other)
388 def is_ancestor_of?(other)
389 self.left < other.left && other.left < self.right && same_scope?(other)
389 self.left < other.left && other.left < self.right && same_scope?(other)
390 end
390 end
391
391
392 def is_or_is_ancestor_of?(other)
392 def is_or_is_ancestor_of?(other)
393 self.left <= other.left && other.left < self.right && same_scope?(other)
393 self.left <= other.left && other.left < self.right && same_scope?(other)
394 end
394 end
395
395
396 # Check if other model is in the same scope
396 # Check if other model is in the same scope
397 def same_scope?(other)
397 def same_scope?(other)
398 Array(acts_as_nested_set_options[:scope]).all? do |attr|
398 Array(acts_as_nested_set_options[:scope]).all? do |attr|
399 self.send(attr) == other.send(attr)
399 self.send(attr) == other.send(attr)
400 end
400 end
401 end
401 end
402
402
403 # Find the first sibling to the left
403 # Find the first sibling to the left
404 def left_sibling
404 def left_sibling
405 siblings.where(["#{quoted_left_column_full_name} < ?", left]).
405 siblings.where(["#{quoted_left_column_full_name} < ?", left]).
406 order("#{quoted_left_column_full_name} DESC").last
406 order("#{quoted_left_column_full_name} DESC").last
407 end
407 end
408
408
409 # Find the first sibling to the right
409 # Find the first sibling to the right
410 def right_sibling
410 def right_sibling
411 siblings.where(["#{quoted_left_column_full_name} > ?", left]).first
411 siblings.where(["#{quoted_left_column_full_name} > ?", left]).first
412 end
412 end
413
413
414 # Shorthand method for finding the left sibling and moving to the left of it.
414 # Shorthand method for finding the left sibling and moving to the left of it.
415 def move_left
415 def move_left
416 move_to_left_of left_sibling
416 move_to_left_of left_sibling
417 end
417 end
418
418
419 # Shorthand method for finding the right sibling and moving to the right of it.
419 # Shorthand method for finding the right sibling and moving to the right of it.
420 def move_right
420 def move_right
421 move_to_right_of right_sibling
421 move_to_right_of right_sibling
422 end
422 end
423
423
424 # Move the node to the left of another node (you can pass id only)
424 # Move the node to the left of another node (you can pass id only)
425 def move_to_left_of(node)
425 def move_to_left_of(node)
426 move_to node, :left
426 move_to node, :left
427 end
427 end
428
428
429 # Move the node to the left of another node (you can pass id only)
429 # Move the node to the left of another node (you can pass id only)
430 def move_to_right_of(node)
430 def move_to_right_of(node)
431 move_to node, :right
431 move_to node, :right
432 end
432 end
433
433
434 # Move the node to the child of another node (you can pass id only)
434 # Move the node to the child of another node (you can pass id only)
435 def move_to_child_of(node)
435 def move_to_child_of(node)
436 move_to node, :child
436 move_to node, :child
437 end
437 end
438
438
439 # Move the node to the child of another node with specify index (you can pass id only)
439 # Move the node to the child of another node with specify index (you can pass id only)
440 def move_to_child_with_index(node, index)
440 def move_to_child_with_index(node, index)
441 if node.children.empty?
441 if node.children.empty?
442 move_to_child_of(node)
442 move_to_child_of(node)
443 elsif node.children.count == index
443 elsif node.children.count == index
444 move_to_right_of(node.children.last)
444 move_to_right_of(node.children.last)
445 else
445 else
446 move_to_left_of(node.children[index])
446 move_to_left_of(node.children[index])
447 end
447 end
448 end
448 end
449
449
450 # Move the node to root nodes
450 # Move the node to root nodes
451 def move_to_root
451 def move_to_root
452 move_to nil, :root
452 move_to nil, :root
453 end
453 end
454
454
455 # Order children in a nested set by an attribute
455 # Order children in a nested set by an attribute
456 # Can order by any attribute class that uses the Comparable mixin, for example a string or integer
456 # Can order by any attribute class that uses the Comparable mixin, for example a string or integer
457 # Usage example when sorting categories alphabetically: @new_category.move_to_ordered_child_of(@root, "name")
457 # Usage example when sorting categories alphabetically: @new_category.move_to_ordered_child_of(@root, "name")
458 def move_to_ordered_child_of(parent, order_attribute, ascending = true)
458 def move_to_ordered_child_of(parent, order_attribute, ascending = true)
459 self.move_to_root and return unless parent
459 self.move_to_root and return unless parent
460 left = nil # This is needed, at least for the tests.
460 left = nil # This is needed, at least for the tests.
461 parent.children.each do |n| # Find the node immediately to the left of this node.
461 parent.children.each do |n| # Find the node immediately to the left of this node.
462 if ascending
462 if ascending
463 left = n if n.send(order_attribute) < self.send(order_attribute)
463 left = n if n.send(order_attribute) < self.send(order_attribute)
464 else
464 else
465 left = n if n.send(order_attribute) > self.send(order_attribute)
465 left = n if n.send(order_attribute) > self.send(order_attribute)
466 end
466 end
467 end
467 end
468 self.move_to_child_of(parent)
468 self.move_to_child_of(parent)
469 return unless parent.children.count > 1 # Only need to order if there are multiple children.
469 return unless parent.children.count > 1 # Only need to order if there are multiple children.
470 if left # Self has a left neighbor.
470 if left # Self has a left neighbor.
471 self.move_to_right_of(left)
471 self.move_to_right_of(left)
472 else # Self is the left most node.
472 else # Self is the left most node.
473 self.move_to_left_of(parent.children[0])
473 self.move_to_left_of(parent.children[0])
474 end
474 end
475 end
475 end
476
476
477 def move_possible?(target)
477 def move_possible?(target)
478 self != target && # Can't target self
478 self != target && # Can't target self
479 same_scope?(target) && # can't be in different scopes
479 same_scope?(target) && # can't be in different scopes
480 # !(left..right).include?(target.left..target.right) # this needs tested more
480 # !(left..right).include?(target.left..target.right) # this needs tested more
481 # detect impossible move
481 # detect impossible move
482 !((left <= target.left && right >= target.left) or (left <= target.right && right >= target.right))
482 !((left <= target.left && right >= target.left) or (left <= target.right && right >= target.right))
483 end
483 end
484
484
485 def to_text
485 def to_text
486 self_and_descendants.map do |node|
486 self_and_descendants.map do |node|
487 "#{'*'*(node.level+1)} #{node.id} #{node.to_s} (#{node.parent_id}, #{node.left}, #{node.right})"
487 "#{'*'*(node.level+1)} #{node.id} #{node.to_s} (#{node.parent_id}, #{node.left}, #{node.right})"
488 end.join("\n")
488 end.join("\n")
489 end
489 end
490
490
491 protected
491 protected
492 def compute_level
492 def compute_level
493 node, nesting = self, 0
493 node, nesting = self, 0
494 while (association = node.association(:parent)).loaded? && association.target
494 while (association = node.association(:parent)).loaded? && association.target
495 nesting += 1
495 nesting += 1
496 node = node.parent
496 node = node.parent
497 end if node.respond_to? :association
497 end if node.respond_to? :association
498 node == self ? ancestors.count : node.level + nesting
498 node == self ? ancestors.count : node.level + nesting
499 end
499 end
500
500
501 def without_self(scope)
501 def without_self(scope)
502 scope.where(["#{self.class.quoted_table_name}.#{self.class.primary_key} != ?", self])
502 scope.where(["#{self.class.quoted_table_name}.#{self.class.primary_key} != ?", self])
503 end
503 end
504
504
505 # All nested set queries should use this nested_set_scope, which performs finds on
505 # All nested set queries should use this nested_set_scope, which performs finds on
506 # the base ActiveRecord class, using the :scope declared in the acts_as_nested_set
506 # the base ActiveRecord class, using the :scope declared in the acts_as_nested_set
507 # declaration.
507 # declaration.
508 def nested_set_scope(options = {})
508 def nested_set_scope(options = {})
509 options = {:order => quoted_left_column_full_name}.merge(options)
509 options = {:order => quoted_left_column_full_name}.merge(options)
510 scopes = Array(acts_as_nested_set_options[:scope])
510 scopes = Array(acts_as_nested_set_options[:scope])
511 options[:conditions] = scopes.inject({}) do |conditions,attr|
511 options[:conditions] = scopes.inject({}) do |conditions,attr|
512 conditions.merge attr => self[attr]
512 conditions.merge attr => self[attr]
513 end unless scopes.empty?
513 end unless scopes.empty?
514 self.class.base_class.unscoped.scoped options
514 self.class.base_class.unscoped.scoped options
515 end
515 end
516
516
517 def store_new_parent
517 def store_new_parent
518 @move_to_new_parent_id = send("#{parent_column_name}_changed?") ? parent_id : false
518 @move_to_new_parent_id = send("#{parent_column_name}_changed?") ? parent_id : false
519 true # force callback to return true
519 true # force callback to return true
520 end
520 end
521
521
522 def move_to_new_parent
522 def move_to_new_parent
523 if @move_to_new_parent_id.nil?
523 if @move_to_new_parent_id.nil?
524 move_to_root
524 move_to_root
525 elsif @move_to_new_parent_id
525 elsif @move_to_new_parent_id
526 move_to_child_of(@move_to_new_parent_id)
526 move_to_child_of(@move_to_new_parent_id)
527 end
527 end
528 end
528 end
529
529
530 def set_depth!
530 def set_depth!
531 if nested_set_scope.column_names.map(&:to_s).include?(depth_column_name.to_s)
531 if nested_set_scope.column_names.map(&:to_s).include?(depth_column_name.to_s)
532 in_tenacious_transaction do
532 in_tenacious_transaction do
533 reload
533 reload
534
534
535 nested_set_scope.where(:id => id).update_all(["#{quoted_depth_column_name} = ?", level])
535 nested_set_scope.where(:id => id).update_all(["#{quoted_depth_column_name} = ?", level])
536 end
536 end
537 self[depth_column_name.to_sym] = self.level
537 self[depth_column_name.to_sym] = self.level
538 end
538 end
539 end
539 end
540
540
541 # on creation, set automatically lft and rgt to the end of the tree
541 # on creation, set automatically lft and rgt to the end of the tree
542 def set_default_left_and_right
542 def set_default_left_and_right
543 highest_right_row = nested_set_scope(:order => "#{quoted_right_column_full_name} desc").limit(1).lock(true).first
543 highest_right_row = nested_set_scope(:order => "#{quoted_right_column_full_name} desc").limit(1).lock(true).first
544 maxright = highest_right_row ? (highest_right_row[right_column_name] || 0) : 0
544 maxright = highest_right_row ? (highest_right_row[right_column_name] || 0) : 0
545 # adds the new node to the right of all existing nodes
545 # adds the new node to the right of all existing nodes
546 self[left_column_name] = maxright + 1
546 self[left_column_name] = maxright + 1
547 self[right_column_name] = maxright + 2
547 self[right_column_name] = maxright + 2
548 end
548 end
549
549
550 def in_tenacious_transaction(&block)
550 def in_tenacious_transaction(&block)
551 retry_count = 0
551 retry_count = 0
552 begin
552 begin
553 transaction(&block)
553 transaction(&block)
554 rescue ActiveRecord::StatementInvalid => error
554 rescue ActiveRecord::StatementInvalid => error
555 raise unless connection.open_transactions.zero?
555 raise unless connection.open_transactions.zero?
556 raise unless error.message =~ /Deadlock found when trying to get lock|Lock wait timeout exceeded/
556 raise unless error.message =~ /Deadlock found when trying to get lock|Lock wait timeout exceeded/
557 raise unless retry_count < 10
557 raise unless retry_count < 10
558 retry_count += 1
558 retry_count += 1
559 logger.info "Deadlock detected on retry #{retry_count}, restarting transaction"
559 logger.info "Deadlock detected on retry #{retry_count}, restarting transaction"
560 sleep(rand(retry_count)*0.1) # Aloha protocol
560 sleep(rand(retry_count)*0.1) # Aloha protocol
561 retry
561 retry
562 end
562 end
563 end
563 end
564
564
565 # Prunes a branch off of the tree, shifting all of the elements on the right
565 # Prunes a branch off of the tree, shifting all of the elements on the right
566 # back to the left so the counts still work.
566 # back to the left so the counts still work.
567 def destroy_descendants
567 def destroy_descendants
568 return if right.nil? || left.nil? || skip_before_destroy
568 return if right.nil? || left.nil? || skip_before_destroy
569
569
570 in_tenacious_transaction do
570 in_tenacious_transaction do
571 reload_nested_set
571 reload_nested_set
572 # select the rows in the model that extend past the deletion point and apply a lock
572 # select the rows in the model that extend past the deletion point and apply a lock
573 nested_set_scope.where(["#{quoted_left_column_full_name} >= ?", left]).
573 nested_set_scope.where(["#{quoted_left_column_full_name} >= ?", left]).
574 select(id).lock(true)
574 select(id).lock(true)
575
575
576 if acts_as_nested_set_options[:dependent] == :destroy
576 if acts_as_nested_set_options[:dependent] == :destroy
577 descendants.each do |model|
577 descendants.each do |model|
578 model.skip_before_destroy = true
578 model.skip_before_destroy = true
579 model.destroy
579 model.destroy
580 end
580 end
581 else
581 else
582 nested_set_scope.where(["#{quoted_left_column_name} > ? AND #{quoted_right_column_name} < ?", left, right]).
582 nested_set_scope.where(["#{quoted_left_column_name} > ? AND #{quoted_right_column_name} < ?", left, right]).
583 delete_all
583 delete_all
584 end
584 end
585
585
586 # update lefts and rights for remaining nodes
586 # update lefts and rights for remaining nodes
587 diff = right - left + 1
587 diff = right - left + 1
588 nested_set_scope.where(["#{quoted_left_column_full_name} > ?", right]).update_all(
588 nested_set_scope.where(["#{quoted_left_column_full_name} > ?", right]).update_all(
589 ["#{quoted_left_column_name} = (#{quoted_left_column_name} - ?)", diff]
589 ["#{quoted_left_column_name} = (#{quoted_left_column_name} - ?)", diff]
590 )
590 )
591
591
592 nested_set_scope.where(["#{quoted_right_column_full_name} > ?", right]).update_all(
592 nested_set_scope.where(["#{quoted_right_column_full_name} > ?", right]).update_all(
593 ["#{quoted_right_column_name} = (#{quoted_right_column_name} - ?)", diff]
593 ["#{quoted_right_column_name} = (#{quoted_right_column_name} - ?)", diff]
594 )
594 )
595
595
596 reload
596 reload
597 # Don't allow multiple calls to destroy to corrupt the set
597 # Don't allow multiple calls to destroy to corrupt the set
598 self.skip_before_destroy = true
598 self.skip_before_destroy = true
599 end
599 end
600 end
600 end
601
601
602 # reload left, right, and parent
602 # reload left, right, and parent
603 def reload_nested_set
603 def reload_nested_set
604 reload(
604 reload(
605 :select => "#{quoted_left_column_full_name}, #{quoted_right_column_full_name}, #{quoted_parent_column_full_name}",
605 :select => "#{quoted_left_column_full_name}, #{quoted_right_column_full_name}, #{quoted_parent_column_full_name}",
606 :lock => true
606 :lock => true
607 )
607 )
608 end
608 end
609
609
610 def move_to(target, position)
610 def move_to(target, position)
611 raise ActiveRecord::ActiveRecordError, "You cannot move a new node" if self.new_record?
611 raise ActiveRecord::ActiveRecordError, "You cannot move a new node" if self.new_record?
612 run_callbacks :move do
612 run_callbacks :move do
613 in_tenacious_transaction do
613 in_tenacious_transaction do
614 if target.is_a? self.class.base_class
614 if target.is_a? self.class.base_class
615 target.reload_nested_set
615 target.reload_nested_set
616 elsif position != :root
616 elsif position != :root
617 # load object if node is not an object
617 # load object if node is not an object
618 target = nested_set_scope.find(target)
618 target = nested_set_scope.find(target)
619 end
619 end
620 self.reload_nested_set
620 self.reload_nested_set
621
621
622 unless position == :root || move_possible?(target)
622 unless position == :root || move_possible?(target)
623 raise ActiveRecord::ActiveRecordError, "Impossible move, target node cannot be inside moved tree."
623 raise ActiveRecord::ActiveRecordError, "Impossible move, target node cannot be inside moved tree."
624 end
624 end
625
625
626 bound = case position
626 bound = case position
627 when :child; target[right_column_name]
627 when :child; target[right_column_name]
628 when :left; target[left_column_name]
628 when :left; target[left_column_name]
629 when :right; target[right_column_name] + 1
629 when :right; target[right_column_name] + 1
630 when :root; 1
630 when :root; 1
631 else raise ActiveRecord::ActiveRecordError, "Position should be :child, :left, :right or :root ('#{position}' received)."
631 else raise ActiveRecord::ActiveRecordError, "Position should be :child, :left, :right or :root ('#{position}' received)."
632 end
632 end
633
633
634 if bound > self[right_column_name]
634 if bound > self[right_column_name]
635 bound = bound - 1
635 bound = bound - 1
636 other_bound = self[right_column_name] + 1
636 other_bound = self[right_column_name] + 1
637 else
637 else
638 other_bound = self[left_column_name] - 1
638 other_bound = self[left_column_name] - 1
639 end
639 end
640
640
641 # there would be no change
641 # there would be no change
642 return if bound == self[right_column_name] || bound == self[left_column_name]
642 return if bound == self[right_column_name] || bound == self[left_column_name]
643
643
644 # we have defined the boundaries of two non-overlapping intervals,
644 # we have defined the boundaries of two non-overlapping intervals,
645 # so sorting puts both the intervals and their boundaries in order
645 # so sorting puts both the intervals and their boundaries in order
646 a, b, c, d = [self[left_column_name], self[right_column_name], bound, other_bound].sort
646 a, b, c, d = [self[left_column_name], self[right_column_name], bound, other_bound].sort
647
647
648 # select the rows in the model between a and d, and apply a lock
648 # select the rows in the model between a and d, and apply a lock
649 self.class.base_class.select('id').lock(true).where(
649 self.class.base_class.select('id').lock(true).where(
650 ["#{quoted_left_column_full_name} >= :a and #{quoted_right_column_full_name} <= :d", {:a => a, :d => d}]
650 ["#{quoted_left_column_full_name} >= :a and #{quoted_right_column_full_name} <= :d", {:a => a, :d => d}]
651 )
651 )
652
652
653 new_parent = case position
653 new_parent = case position
654 when :child; target.id
654 when :child; target.id
655 when :root; nil
655 when :root; nil
656 else target[parent_column_name]
656 else target[parent_column_name]
657 end
657 end
658
658
659 self.nested_set_scope.update_all([
659 self.nested_set_scope.update_all([
660 "#{quoted_left_column_name} = CASE " +
660 "#{quoted_left_column_name} = CASE " +
661 "WHEN #{quoted_left_column_name} BETWEEN :a AND :b " +
661 "WHEN #{quoted_left_column_name} BETWEEN :a AND :b " +
662 "THEN #{quoted_left_column_name} + :d - :b " +
662 "THEN #{quoted_left_column_name} + :d - :b " +
663 "WHEN #{quoted_left_column_name} BETWEEN :c AND :d " +
663 "WHEN #{quoted_left_column_name} BETWEEN :c AND :d " +
664 "THEN #{quoted_left_column_name} + :a - :c " +
664 "THEN #{quoted_left_column_name} + :a - :c " +
665 "ELSE #{quoted_left_column_name} END, " +
665 "ELSE #{quoted_left_column_name} END, " +
666 "#{quoted_right_column_name} = CASE " +
666 "#{quoted_right_column_name} = CASE " +
667 "WHEN #{quoted_right_column_name} BETWEEN :a AND :b " +
667 "WHEN #{quoted_right_column_name} BETWEEN :a AND :b " +
668 "THEN #{quoted_right_column_name} + :d - :b " +
668 "THEN #{quoted_right_column_name} + :d - :b " +
669 "WHEN #{quoted_right_column_name} BETWEEN :c AND :d " +
669 "WHEN #{quoted_right_column_name} BETWEEN :c AND :d " +
670 "THEN #{quoted_right_column_name} + :a - :c " +
670 "THEN #{quoted_right_column_name} + :a - :c " +
671 "ELSE #{quoted_right_column_name} END, " +
671 "ELSE #{quoted_right_column_name} END, " +
672 "#{quoted_parent_column_name} = CASE " +
672 "#{quoted_parent_column_name} = CASE " +
673 "WHEN #{self.class.base_class.primary_key} = :id THEN :new_parent " +
673 "WHEN #{self.class.base_class.primary_key} = :id THEN :new_parent " +
674 "ELSE #{quoted_parent_column_name} END",
674 "ELSE #{quoted_parent_column_name} END",
675 {:a => a, :b => b, :c => c, :d => d, :id => self.id, :new_parent => new_parent}
675 {:a => a, :b => b, :c => c, :d => d, :id => self.id, :new_parent => new_parent}
676 ])
676 ])
677 end
677 end
678 target.reload_nested_set if target
678 target.reload_nested_set if target
679 self.set_depth!
679 self.set_depth!
680 self.descendants.each(&:save)
680 self.descendants.each(&:save)
681 self.reload_nested_set
681 self.reload_nested_set
682 end
682 end
683 end
683 end
684
684
685 end
685 end
686
686
687 # Mixed into both classes and instances to provide easy access to the column names
687 # Mixed into both classes and instances to provide easy access to the column names
688 module Columns
688 module Columns
689 def left_column_name
689 def left_column_name
690 acts_as_nested_set_options[:left_column]
690 acts_as_nested_set_options[:left_column]
691 end
691 end
692
692
693 def right_column_name
693 def right_column_name
694 acts_as_nested_set_options[:right_column]
694 acts_as_nested_set_options[:right_column]
695 end
695 end
696
696
697 def depth_column_name
697 def depth_column_name
698 acts_as_nested_set_options[:depth_column]
698 acts_as_nested_set_options[:depth_column]
699 end
699 end
700
700
701 def parent_column_name
701 def parent_column_name
702 acts_as_nested_set_options[:parent_column]
702 acts_as_nested_set_options[:parent_column]
703 end
703 end
704
704
705 def order_column
705 def order_column
706 acts_as_nested_set_options[:order_column] || left_column_name
706 acts_as_nested_set_options[:order_column] || left_column_name
707 end
707 end
708
708
709 def scope_column_names
709 def scope_column_names
710 Array(acts_as_nested_set_options[:scope])
710 Array(acts_as_nested_set_options[:scope])
711 end
711 end
712
712
713 def quoted_left_column_name
713 def quoted_left_column_name
714 connection.quote_column_name(left_column_name)
714 connection.quote_column_name(left_column_name)
715 end
715 end
716
716
717 def quoted_right_column_name
717 def quoted_right_column_name
718 connection.quote_column_name(right_column_name)
718 connection.quote_column_name(right_column_name)
719 end
719 end
720
720
721 def quoted_depth_column_name
721 def quoted_depth_column_name
722 connection.quote_column_name(depth_column_name)
722 connection.quote_column_name(depth_column_name)
723 end
723 end
724
724
725 def quoted_parent_column_name
725 def quoted_parent_column_name
726 connection.quote_column_name(parent_column_name)
726 connection.quote_column_name(parent_column_name)
727 end
727 end
728
728
729 def quoted_scope_column_names
729 def quoted_scope_column_names
730 scope_column_names.collect {|column_name| connection.quote_column_name(column_name) }
730 scope_column_names.collect {|column_name| connection.quote_column_name(column_name) }
731 end
731 end
732
732
733 def quoted_left_column_full_name
733 def quoted_left_column_full_name
734 "#{quoted_table_name}.#{quoted_left_column_name}"
734 "#{quoted_table_name}.#{quoted_left_column_name}"
735 end
735 end
736
736
737 def quoted_right_column_full_name
737 def quoted_right_column_full_name
738 "#{quoted_table_name}.#{quoted_right_column_name}"
738 "#{quoted_table_name}.#{quoted_right_column_name}"
739 end
739 end
740
740
741 def quoted_parent_column_full_name
741 def quoted_parent_column_full_name
742 "#{quoted_table_name}.#{quoted_parent_column_name}"
742 "#{quoted_table_name}.#{quoted_parent_column_name}"
743 end
743 end
744 end
744 end
745
745
746 end
746 end
747 end
747 end
748 end
748 end
@@ -1,404 +1,409
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
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
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.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19
19
20 class IssueNestedSetTest < ActiveSupport::TestCase
20 class IssueNestedSetTest < ActiveSupport::TestCase
21 fixtures :projects, :users, :roles,
21 fixtures :projects, :users, :roles,
22 :trackers, :projects_trackers,
22 :trackers, :projects_trackers,
23 :issue_statuses, :issue_categories, :issue_relations,
23 :issue_statuses, :issue_categories, :issue_relations,
24 :enumerations,
24 :enumerations,
25 :issues
25 :issues
26
26
27 def test_new_record_is_leaf
28 i = Issue.new
29 assert i.leaf?
30 end
31
27 def test_create_root_issue
32 def test_create_root_issue
28 issue1 = Issue.generate!
33 issue1 = Issue.generate!
29 issue2 = Issue.generate!
34 issue2 = Issue.generate!
30 issue1.reload
35 issue1.reload
31 issue2.reload
36 issue2.reload
32
37
33 assert_equal [issue1.id, nil, 1, 2], [issue1.root_id, issue1.parent_id, issue1.lft, issue1.rgt]
38 assert_equal [issue1.id, nil, 1, 2], [issue1.root_id, issue1.parent_id, issue1.lft, issue1.rgt]
34 assert_equal [issue2.id, nil, 1, 2], [issue2.root_id, issue2.parent_id, issue2.lft, issue2.rgt]
39 assert_equal [issue2.id, nil, 1, 2], [issue2.root_id, issue2.parent_id, issue2.lft, issue2.rgt]
35 end
40 end
36
41
37 def test_create_child_issue
42 def test_create_child_issue
38 parent = Issue.generate!
43 parent = Issue.generate!
39 child = Issue.generate!(:parent_issue_id => parent.id)
44 child = Issue.generate!(:parent_issue_id => parent.id)
40 parent.reload
45 parent.reload
41 child.reload
46 child.reload
42
47
43 assert_equal [parent.id, nil, 1, 4], [parent.root_id, parent.parent_id, parent.lft, parent.rgt]
48 assert_equal [parent.id, nil, 1, 4], [parent.root_id, parent.parent_id, parent.lft, parent.rgt]
44 assert_equal [parent.id, parent.id, 2, 3], [child.root_id, child.parent_id, child.lft, child.rgt]
49 assert_equal [parent.id, parent.id, 2, 3], [child.root_id, child.parent_id, child.lft, child.rgt]
45 end
50 end
46
51
47 def test_creating_a_child_in_a_subproject_should_validate
52 def test_creating_a_child_in_a_subproject_should_validate
48 issue = Issue.generate!
53 issue = Issue.generate!
49 child = Issue.new(:project_id => 3, :tracker_id => 2, :author_id => 1,
54 child = Issue.new(:project_id => 3, :tracker_id => 2, :author_id => 1,
50 :subject => 'child', :parent_issue_id => issue.id)
55 :subject => 'child', :parent_issue_id => issue.id)
51 assert_save child
56 assert_save child
52 assert_equal issue, child.reload.parent
57 assert_equal issue, child.reload.parent
53 end
58 end
54
59
55 def test_creating_a_child_in_an_invalid_project_should_not_validate
60 def test_creating_a_child_in_an_invalid_project_should_not_validate
56 issue = Issue.generate!
61 issue = Issue.generate!
57 child = Issue.new(:project_id => 2, :tracker_id => 1, :author_id => 1,
62 child = Issue.new(:project_id => 2, :tracker_id => 1, :author_id => 1,
58 :subject => 'child', :parent_issue_id => issue.id)
63 :subject => 'child', :parent_issue_id => issue.id)
59 assert !child.save
64 assert !child.save
60 assert_not_equal [], child.errors[:parent_issue_id]
65 assert_not_equal [], child.errors[:parent_issue_id]
61 end
66 end
62
67
63 def test_move_a_root_to_child
68 def test_move_a_root_to_child
64 parent1 = Issue.generate!
69 parent1 = Issue.generate!
65 parent2 = Issue.generate!
70 parent2 = Issue.generate!
66 child = Issue.generate!(:parent_issue_id => parent1.id)
71 child = Issue.generate!(:parent_issue_id => parent1.id)
67
72
68 parent2.parent_issue_id = parent1.id
73 parent2.parent_issue_id = parent1.id
69 parent2.save!
74 parent2.save!
70 child.reload
75 child.reload
71 parent1.reload
76 parent1.reload
72 parent2.reload
77 parent2.reload
73
78
74 assert_equal [parent1.id, 1, 6], [parent1.root_id, parent1.lft, parent1.rgt]
79 assert_equal [parent1.id, 1, 6], [parent1.root_id, parent1.lft, parent1.rgt]
75 assert_equal [parent1.id, 4, 5], [parent2.root_id, parent2.lft, parent2.rgt]
80 assert_equal [parent1.id, 4, 5], [parent2.root_id, parent2.lft, parent2.rgt]
76 assert_equal [parent1.id, 2, 3], [child.root_id, child.lft, child.rgt]
81 assert_equal [parent1.id, 2, 3], [child.root_id, child.lft, child.rgt]
77 end
82 end
78
83
79 def test_move_a_child_to_root
84 def test_move_a_child_to_root
80 parent1 = Issue.generate!
85 parent1 = Issue.generate!
81 parent2 = Issue.generate!
86 parent2 = Issue.generate!
82 child = Issue.generate!(:parent_issue_id => parent1.id)
87 child = Issue.generate!(:parent_issue_id => parent1.id)
83
88
84 child.parent_issue_id = nil
89 child.parent_issue_id = nil
85 child.save!
90 child.save!
86 child.reload
91 child.reload
87 parent1.reload
92 parent1.reload
88 parent2.reload
93 parent2.reload
89
94
90 assert_equal [parent1.id, 1, 2], [parent1.root_id, parent1.lft, parent1.rgt]
95 assert_equal [parent1.id, 1, 2], [parent1.root_id, parent1.lft, parent1.rgt]
91 assert_equal [parent2.id, 1, 2], [parent2.root_id, parent2.lft, parent2.rgt]
96 assert_equal [parent2.id, 1, 2], [parent2.root_id, parent2.lft, parent2.rgt]
92 assert_equal [child.id, 1, 2], [child.root_id, child.lft, child.rgt]
97 assert_equal [child.id, 1, 2], [child.root_id, child.lft, child.rgt]
93 end
98 end
94
99
95 def test_move_a_child_to_another_issue
100 def test_move_a_child_to_another_issue
96 parent1 = Issue.generate!
101 parent1 = Issue.generate!
97 parent2 = Issue.generate!
102 parent2 = Issue.generate!
98 child = Issue.generate!(:parent_issue_id => parent1.id)
103 child = Issue.generate!(:parent_issue_id => parent1.id)
99
104
100 child.parent_issue_id = parent2.id
105 child.parent_issue_id = parent2.id
101 child.save!
106 child.save!
102 child.reload
107 child.reload
103 parent1.reload
108 parent1.reload
104 parent2.reload
109 parent2.reload
105
110
106 assert_equal [parent1.id, 1, 2], [parent1.root_id, parent1.lft, parent1.rgt]
111 assert_equal [parent1.id, 1, 2], [parent1.root_id, parent1.lft, parent1.rgt]
107 assert_equal [parent2.id, 1, 4], [parent2.root_id, parent2.lft, parent2.rgt]
112 assert_equal [parent2.id, 1, 4], [parent2.root_id, parent2.lft, parent2.rgt]
108 assert_equal [parent2.id, 2, 3], [child.root_id, child.lft, child.rgt]
113 assert_equal [parent2.id, 2, 3], [child.root_id, child.lft, child.rgt]
109 end
114 end
110
115
111 def test_move_a_child_with_descendants_to_another_issue
116 def test_move_a_child_with_descendants_to_another_issue
112 parent1 = Issue.generate!
117 parent1 = Issue.generate!
113 parent2 = Issue.generate!
118 parent2 = Issue.generate!
114 child = Issue.generate!(:parent_issue_id => parent1.id)
119 child = Issue.generate!(:parent_issue_id => parent1.id)
115 grandchild = Issue.generate!(:parent_issue_id => child.id)
120 grandchild = Issue.generate!(:parent_issue_id => child.id)
116
121
117 parent1.reload
122 parent1.reload
118 parent2.reload
123 parent2.reload
119 child.reload
124 child.reload
120 grandchild.reload
125 grandchild.reload
121
126
122 assert_equal [parent1.id, 1, 6], [parent1.root_id, parent1.lft, parent1.rgt]
127 assert_equal [parent1.id, 1, 6], [parent1.root_id, parent1.lft, parent1.rgt]
123 assert_equal [parent2.id, 1, 2], [parent2.root_id, parent2.lft, parent2.rgt]
128 assert_equal [parent2.id, 1, 2], [parent2.root_id, parent2.lft, parent2.rgt]
124 assert_equal [parent1.id, 2, 5], [child.root_id, child.lft, child.rgt]
129 assert_equal [parent1.id, 2, 5], [child.root_id, child.lft, child.rgt]
125 assert_equal [parent1.id, 3, 4], [grandchild.root_id, grandchild.lft, grandchild.rgt]
130 assert_equal [parent1.id, 3, 4], [grandchild.root_id, grandchild.lft, grandchild.rgt]
126
131
127 child.reload.parent_issue_id = parent2.id
132 child.reload.parent_issue_id = parent2.id
128 child.save!
133 child.save!
129 child.reload
134 child.reload
130 grandchild.reload
135 grandchild.reload
131 parent1.reload
136 parent1.reload
132 parent2.reload
137 parent2.reload
133
138
134 assert_equal [parent1.id, 1, 2], [parent1.root_id, parent1.lft, parent1.rgt]
139 assert_equal [parent1.id, 1, 2], [parent1.root_id, parent1.lft, parent1.rgt]
135 assert_equal [parent2.id, 1, 6], [parent2.root_id, parent2.lft, parent2.rgt]
140 assert_equal [parent2.id, 1, 6], [parent2.root_id, parent2.lft, parent2.rgt]
136 assert_equal [parent2.id, 2, 5], [child.root_id, child.lft, child.rgt]
141 assert_equal [parent2.id, 2, 5], [child.root_id, child.lft, child.rgt]
137 assert_equal [parent2.id, 3, 4], [grandchild.root_id, grandchild.lft, grandchild.rgt]
142 assert_equal [parent2.id, 3, 4], [grandchild.root_id, grandchild.lft, grandchild.rgt]
138 end
143 end
139
144
140 def test_move_a_child_with_descendants_to_another_project
145 def test_move_a_child_with_descendants_to_another_project
141 parent1 = Issue.generate!
146 parent1 = Issue.generate!
142 child = Issue.generate!(:parent_issue_id => parent1.id)
147 child = Issue.generate!(:parent_issue_id => parent1.id)
143 grandchild = Issue.generate!(:parent_issue_id => child.id)
148 grandchild = Issue.generate!(:parent_issue_id => child.id)
144
149
145 child.reload
150 child.reload
146 child.project = Project.find(2)
151 child.project = Project.find(2)
147 assert child.save
152 assert child.save
148 child.reload
153 child.reload
149 grandchild.reload
154 grandchild.reload
150 parent1.reload
155 parent1.reload
151
156
152 assert_equal [1, parent1.id, 1, 2], [parent1.project_id, parent1.root_id, parent1.lft, parent1.rgt]
157 assert_equal [1, parent1.id, 1, 2], [parent1.project_id, parent1.root_id, parent1.lft, parent1.rgt]
153 assert_equal [2, child.id, 1, 4], [child.project_id, child.root_id, child.lft, child.rgt]
158 assert_equal [2, child.id, 1, 4], [child.project_id, child.root_id, child.lft, child.rgt]
154 assert_equal [2, child.id, 2, 3], [grandchild.project_id, grandchild.root_id, grandchild.lft, grandchild.rgt]
159 assert_equal [2, child.id, 2, 3], [grandchild.project_id, grandchild.root_id, grandchild.lft, grandchild.rgt]
155 end
160 end
156
161
157 def test_moving_an_issue_to_a_descendant_should_not_validate
162 def test_moving_an_issue_to_a_descendant_should_not_validate
158 parent1 = Issue.generate!
163 parent1 = Issue.generate!
159 parent2 = Issue.generate!
164 parent2 = Issue.generate!
160 child = Issue.generate!(:parent_issue_id => parent1.id)
165 child = Issue.generate!(:parent_issue_id => parent1.id)
161 grandchild = Issue.generate!(:parent_issue_id => child.id)
166 grandchild = Issue.generate!(:parent_issue_id => child.id)
162
167
163 child.reload
168 child.reload
164 child.parent_issue_id = grandchild.id
169 child.parent_issue_id = grandchild.id
165 assert !child.save
170 assert !child.save
166 assert_not_equal [], child.errors[:parent_issue_id]
171 assert_not_equal [], child.errors[:parent_issue_id]
167 end
172 end
168
173
169 def test_updating_a_root_issue_should_not_trigger_update_nested_set_attributes_on_parent_change
174 def test_updating_a_root_issue_should_not_trigger_update_nested_set_attributes_on_parent_change
170 issue = Issue.find(Issue.generate!.id)
175 issue = Issue.find(Issue.generate!.id)
171 issue.parent_issue_id = ""
176 issue.parent_issue_id = ""
172 issue.expects(:update_nested_set_attributes_on_parent_change).never
177 issue.expects(:update_nested_set_attributes_on_parent_change).never
173 issue.save!
178 issue.save!
174 end
179 end
175
180
176 def test_updating_a_child_issue_should_not_trigger_update_nested_set_attributes_on_parent_change
181 def test_updating_a_child_issue_should_not_trigger_update_nested_set_attributes_on_parent_change
177 issue = Issue.find(Issue.generate!(:parent_issue_id => 1).id)
182 issue = Issue.find(Issue.generate!(:parent_issue_id => 1).id)
178 issue.parent_issue_id = "1"
183 issue.parent_issue_id = "1"
179 issue.expects(:update_nested_set_attributes_on_parent_change).never
184 issue.expects(:update_nested_set_attributes_on_parent_change).never
180 issue.save!
185 issue.save!
181 end
186 end
182
187
183 def test_moving_a_root_issue_should_trigger_update_nested_set_attributes_on_parent_change
188 def test_moving_a_root_issue_should_trigger_update_nested_set_attributes_on_parent_change
184 issue = Issue.find(Issue.generate!.id)
189 issue = Issue.find(Issue.generate!.id)
185 issue.parent_issue_id = "1"
190 issue.parent_issue_id = "1"
186 issue.expects(:update_nested_set_attributes_on_parent_change).once
191 issue.expects(:update_nested_set_attributes_on_parent_change).once
187 issue.save!
192 issue.save!
188 end
193 end
189
194
190 def test_moving_a_child_issue_to_another_parent_should_trigger_update_nested_set_attributes_on_parent_change
195 def test_moving_a_child_issue_to_another_parent_should_trigger_update_nested_set_attributes_on_parent_change
191 issue = Issue.find(Issue.generate!(:parent_issue_id => 1).id)
196 issue = Issue.find(Issue.generate!(:parent_issue_id => 1).id)
192 issue.parent_issue_id = "2"
197 issue.parent_issue_id = "2"
193 issue.expects(:update_nested_set_attributes_on_parent_change).once
198 issue.expects(:update_nested_set_attributes_on_parent_change).once
194 issue.save!
199 issue.save!
195 end
200 end
196
201
197 def test_moving_a_child_issue_to_root_should_trigger_update_nested_set_attributes_on_parent_change
202 def test_moving_a_child_issue_to_root_should_trigger_update_nested_set_attributes_on_parent_change
198 issue = Issue.find(Issue.generate!(:parent_issue_id => 1).id)
203 issue = Issue.find(Issue.generate!(:parent_issue_id => 1).id)
199 issue.parent_issue_id = ""
204 issue.parent_issue_id = ""
200 issue.expects(:update_nested_set_attributes_on_parent_change).once
205 issue.expects(:update_nested_set_attributes_on_parent_change).once
201 issue.save!
206 issue.save!
202 end
207 end
203
208
204 def test_destroy_should_destroy_children
209 def test_destroy_should_destroy_children
205 issue1 = Issue.generate!
210 issue1 = Issue.generate!
206 issue2 = Issue.generate!
211 issue2 = Issue.generate!
207 issue3 = Issue.generate!(:parent_issue_id => issue2.id)
212 issue3 = Issue.generate!(:parent_issue_id => issue2.id)
208 issue4 = Issue.generate!(:parent_issue_id => issue1.id)
213 issue4 = Issue.generate!(:parent_issue_id => issue1.id)
209
214
210 issue3.init_journal(User.find(2))
215 issue3.init_journal(User.find(2))
211 issue3.subject = 'child with journal'
216 issue3.subject = 'child with journal'
212 issue3.save!
217 issue3.save!
213
218
214 assert_difference 'Issue.count', -2 do
219 assert_difference 'Issue.count', -2 do
215 assert_difference 'Journal.count', -1 do
220 assert_difference 'Journal.count', -1 do
216 assert_difference 'JournalDetail.count', -1 do
221 assert_difference 'JournalDetail.count', -1 do
217 Issue.find(issue2.id).destroy
222 Issue.find(issue2.id).destroy
218 end
223 end
219 end
224 end
220 end
225 end
221
226
222 issue1.reload
227 issue1.reload
223 issue4.reload
228 issue4.reload
224 assert !Issue.exists?(issue2.id)
229 assert !Issue.exists?(issue2.id)
225 assert !Issue.exists?(issue3.id)
230 assert !Issue.exists?(issue3.id)
226 assert_equal [issue1.id, 1, 4], [issue1.root_id, issue1.lft, issue1.rgt]
231 assert_equal [issue1.id, 1, 4], [issue1.root_id, issue1.lft, issue1.rgt]
227 assert_equal [issue1.id, 2, 3], [issue4.root_id, issue4.lft, issue4.rgt]
232 assert_equal [issue1.id, 2, 3], [issue4.root_id, issue4.lft, issue4.rgt]
228 end
233 end
229
234
230 def test_destroy_child_should_update_parent
235 def test_destroy_child_should_update_parent
231 issue = Issue.generate!
236 issue = Issue.generate!
232 child1 = Issue.generate!(:parent_issue_id => issue.id)
237 child1 = Issue.generate!(:parent_issue_id => issue.id)
233 child2 = Issue.generate!(:parent_issue_id => issue.id)
238 child2 = Issue.generate!(:parent_issue_id => issue.id)
234
239
235 issue.reload
240 issue.reload
236 assert_equal [issue.id, 1, 6], [issue.root_id, issue.lft, issue.rgt]
241 assert_equal [issue.id, 1, 6], [issue.root_id, issue.lft, issue.rgt]
237
242
238 child2.reload.destroy
243 child2.reload.destroy
239
244
240 issue.reload
245 issue.reload
241 assert_equal [issue.id, 1, 4], [issue.root_id, issue.lft, issue.rgt]
246 assert_equal [issue.id, 1, 4], [issue.root_id, issue.lft, issue.rgt]
242 end
247 end
243
248
244 def test_destroy_parent_issue_updated_during_children_destroy
249 def test_destroy_parent_issue_updated_during_children_destroy
245 parent = Issue.generate!
250 parent = Issue.generate!
246 Issue.generate!(:start_date => Date.today, :parent_issue_id => parent.id)
251 Issue.generate!(:start_date => Date.today, :parent_issue_id => parent.id)
247 Issue.generate!(:start_date => 2.days.from_now, :parent_issue_id => parent.id)
252 Issue.generate!(:start_date => 2.days.from_now, :parent_issue_id => parent.id)
248
253
249 assert_difference 'Issue.count', -3 do
254 assert_difference 'Issue.count', -3 do
250 Issue.find(parent.id).destroy
255 Issue.find(parent.id).destroy
251 end
256 end
252 end
257 end
253
258
254 def test_destroy_child_issue_with_children
259 def test_destroy_child_issue_with_children
255 root = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'root')
260 root = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'root')
256 child = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'child', :parent_issue_id => root.id)
261 child = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'child', :parent_issue_id => root.id)
257 leaf = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'leaf', :parent_issue_id => child.id)
262 leaf = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'leaf', :parent_issue_id => child.id)
258 leaf.init_journal(User.find(2))
263 leaf.init_journal(User.find(2))
259 leaf.subject = 'leaf with journal'
264 leaf.subject = 'leaf with journal'
260 leaf.save!
265 leaf.save!
261
266
262 assert_difference 'Issue.count', -2 do
267 assert_difference 'Issue.count', -2 do
263 assert_difference 'Journal.count', -1 do
268 assert_difference 'Journal.count', -1 do
264 assert_difference 'JournalDetail.count', -1 do
269 assert_difference 'JournalDetail.count', -1 do
265 Issue.find(child.id).destroy
270 Issue.find(child.id).destroy
266 end
271 end
267 end
272 end
268 end
273 end
269
274
270 root = Issue.find(root.id)
275 root = Issue.find(root.id)
271 assert root.leaf?, "Root issue is not a leaf (lft: #{root.lft}, rgt: #{root.rgt})"
276 assert root.leaf?, "Root issue is not a leaf (lft: #{root.lft}, rgt: #{root.rgt})"
272 end
277 end
273
278
274 def test_destroy_issue_with_grand_child
279 def test_destroy_issue_with_grand_child
275 parent = Issue.generate!
280 parent = Issue.generate!
276 issue = Issue.generate!(:parent_issue_id => parent.id)
281 issue = Issue.generate!(:parent_issue_id => parent.id)
277 child = Issue.generate!(:parent_issue_id => issue.id)
282 child = Issue.generate!(:parent_issue_id => issue.id)
278 grandchild1 = Issue.generate!(:parent_issue_id => child.id)
283 grandchild1 = Issue.generate!(:parent_issue_id => child.id)
279 grandchild2 = Issue.generate!(:parent_issue_id => child.id)
284 grandchild2 = Issue.generate!(:parent_issue_id => child.id)
280
285
281 assert_difference 'Issue.count', -4 do
286 assert_difference 'Issue.count', -4 do
282 Issue.find(issue.id).destroy
287 Issue.find(issue.id).destroy
283 parent.reload
288 parent.reload
284 assert_equal [1, 2], [parent.lft, parent.rgt]
289 assert_equal [1, 2], [parent.lft, parent.rgt]
285 end
290 end
286 end
291 end
287
292
288 def test_parent_priority_should_be_the_highest_child_priority
293 def test_parent_priority_should_be_the_highest_child_priority
289 parent = Issue.generate!(:priority => IssuePriority.find_by_name('Normal'))
294 parent = Issue.generate!(:priority => IssuePriority.find_by_name('Normal'))
290 # Create children
295 # Create children
291 child1 = Issue.generate!(:priority => IssuePriority.find_by_name('High'), :parent_issue_id => parent.id)
296 child1 = Issue.generate!(:priority => IssuePriority.find_by_name('High'), :parent_issue_id => parent.id)
292 assert_equal 'High', parent.reload.priority.name
297 assert_equal 'High', parent.reload.priority.name
293 child2 = Issue.generate!(:priority => IssuePriority.find_by_name('Immediate'), :parent_issue_id => child1.id)
298 child2 = Issue.generate!(:priority => IssuePriority.find_by_name('Immediate'), :parent_issue_id => child1.id)
294 assert_equal 'Immediate', child1.reload.priority.name
299 assert_equal 'Immediate', child1.reload.priority.name
295 assert_equal 'Immediate', parent.reload.priority.name
300 assert_equal 'Immediate', parent.reload.priority.name
296 child3 = Issue.generate!(:priority => IssuePriority.find_by_name('Low'), :parent_issue_id => parent.id)
301 child3 = Issue.generate!(:priority => IssuePriority.find_by_name('Low'), :parent_issue_id => parent.id)
297 assert_equal 'Immediate', parent.reload.priority.name
302 assert_equal 'Immediate', parent.reload.priority.name
298 # Destroy a child
303 # Destroy a child
299 child1.destroy
304 child1.destroy
300 assert_equal 'Low', parent.reload.priority.name
305 assert_equal 'Low', parent.reload.priority.name
301 # Update a child
306 # Update a child
302 child3.reload.priority = IssuePriority.find_by_name('Normal')
307 child3.reload.priority = IssuePriority.find_by_name('Normal')
303 child3.save!
308 child3.save!
304 assert_equal 'Normal', parent.reload.priority.name
309 assert_equal 'Normal', parent.reload.priority.name
305 end
310 end
306
311
307 def test_parent_dates_should_be_lowest_start_and_highest_due_dates
312 def test_parent_dates_should_be_lowest_start_and_highest_due_dates
308 parent = Issue.generate!
313 parent = Issue.generate!
309 Issue.generate!(:start_date => '2010-01-25', :due_date => '2010-02-15', :parent_issue_id => parent.id)
314 Issue.generate!(:start_date => '2010-01-25', :due_date => '2010-02-15', :parent_issue_id => parent.id)
310 Issue.generate!( :due_date => '2010-02-13', :parent_issue_id => parent.id)
315 Issue.generate!( :due_date => '2010-02-13', :parent_issue_id => parent.id)
311 Issue.generate!(:start_date => '2010-02-01', :due_date => '2010-02-22', :parent_issue_id => parent.id)
316 Issue.generate!(:start_date => '2010-02-01', :due_date => '2010-02-22', :parent_issue_id => parent.id)
312 parent.reload
317 parent.reload
313 assert_equal Date.parse('2010-01-25'), parent.start_date
318 assert_equal Date.parse('2010-01-25'), parent.start_date
314 assert_equal Date.parse('2010-02-22'), parent.due_date
319 assert_equal Date.parse('2010-02-22'), parent.due_date
315 end
320 end
316
321
317 def test_parent_done_ratio_should_be_average_done_ratio_of_leaves
322 def test_parent_done_ratio_should_be_average_done_ratio_of_leaves
318 parent = Issue.generate!
323 parent = Issue.generate!
319 Issue.generate!(:done_ratio => 20, :parent_issue_id => parent.id)
324 Issue.generate!(:done_ratio => 20, :parent_issue_id => parent.id)
320 assert_equal 20, parent.reload.done_ratio
325 assert_equal 20, parent.reload.done_ratio
321 Issue.generate!(:done_ratio => 70, :parent_issue_id => parent.id)
326 Issue.generate!(:done_ratio => 70, :parent_issue_id => parent.id)
322 assert_equal 45, parent.reload.done_ratio
327 assert_equal 45, parent.reload.done_ratio
323
328
324 child = Issue.generate!(:done_ratio => 0, :parent_issue_id => parent.id)
329 child = Issue.generate!(:done_ratio => 0, :parent_issue_id => parent.id)
325 assert_equal 30, parent.reload.done_ratio
330 assert_equal 30, parent.reload.done_ratio
326
331
327 Issue.generate!(:done_ratio => 30, :parent_issue_id => child.id)
332 Issue.generate!(:done_ratio => 30, :parent_issue_id => child.id)
328 assert_equal 30, child.reload.done_ratio
333 assert_equal 30, child.reload.done_ratio
329 assert_equal 40, parent.reload.done_ratio
334 assert_equal 40, parent.reload.done_ratio
330 end
335 end
331
336
332 def test_parent_done_ratio_should_be_weighted_by_estimated_times_if_any
337 def test_parent_done_ratio_should_be_weighted_by_estimated_times_if_any
333 parent = Issue.generate!
338 parent = Issue.generate!
334 Issue.generate!(:estimated_hours => 10, :done_ratio => 20, :parent_issue_id => parent.id)
339 Issue.generate!(:estimated_hours => 10, :done_ratio => 20, :parent_issue_id => parent.id)
335 assert_equal 20, parent.reload.done_ratio
340 assert_equal 20, parent.reload.done_ratio
336 Issue.generate!(:estimated_hours => 20, :done_ratio => 50, :parent_issue_id => parent.id)
341 Issue.generate!(:estimated_hours => 20, :done_ratio => 50, :parent_issue_id => parent.id)
337 assert_equal (50 * 20 + 20 * 10) / 30, parent.reload.done_ratio
342 assert_equal (50 * 20 + 20 * 10) / 30, parent.reload.done_ratio
338 end
343 end
339
344
340 def test_parent_done_ratio_with_child_estimate_to_0_should_reach_100
345 def test_parent_done_ratio_with_child_estimate_to_0_should_reach_100
341 parent = Issue.generate!
346 parent = Issue.generate!
342 issue1 = Issue.generate!(:parent_issue_id => parent.id)
347 issue1 = Issue.generate!(:parent_issue_id => parent.id)
343 issue2 = Issue.generate!(:parent_issue_id => parent.id, :estimated_hours => 0)
348 issue2 = Issue.generate!(:parent_issue_id => parent.id, :estimated_hours => 0)
344 assert_equal 0, parent.reload.done_ratio
349 assert_equal 0, parent.reload.done_ratio
345 issue1.reload.update_attribute :status_id, 5
350 issue1.reload.update_attribute :status_id, 5
346 assert_equal 50, parent.reload.done_ratio
351 assert_equal 50, parent.reload.done_ratio
347 issue2.reload.update_attribute :status_id, 5
352 issue2.reload.update_attribute :status_id, 5
348 assert_equal 100, parent.reload.done_ratio
353 assert_equal 100, parent.reload.done_ratio
349 end
354 end
350
355
351 def test_parent_estimate_should_be_sum_of_leaves
356 def test_parent_estimate_should_be_sum_of_leaves
352 parent = Issue.generate!
357 parent = Issue.generate!
353 Issue.generate!(:estimated_hours => nil, :parent_issue_id => parent.id)
358 Issue.generate!(:estimated_hours => nil, :parent_issue_id => parent.id)
354 assert_equal nil, parent.reload.estimated_hours
359 assert_equal nil, parent.reload.estimated_hours
355 Issue.generate!(:estimated_hours => 5, :parent_issue_id => parent.id)
360 Issue.generate!(:estimated_hours => 5, :parent_issue_id => parent.id)
356 assert_equal 5, parent.reload.estimated_hours
361 assert_equal 5, parent.reload.estimated_hours
357 Issue.generate!(:estimated_hours => 7, :parent_issue_id => parent.id)
362 Issue.generate!(:estimated_hours => 7, :parent_issue_id => parent.id)
358 assert_equal 12, parent.reload.estimated_hours
363 assert_equal 12, parent.reload.estimated_hours
359 end
364 end
360
365
361 def test_move_parent_updates_old_parent_attributes
366 def test_move_parent_updates_old_parent_attributes
362 first_parent = Issue.generate!
367 first_parent = Issue.generate!
363 second_parent = Issue.generate!
368 second_parent = Issue.generate!
364 child = Issue.generate!(:estimated_hours => 5, :parent_issue_id => first_parent.id)
369 child = Issue.generate!(:estimated_hours => 5, :parent_issue_id => first_parent.id)
365 assert_equal 5, first_parent.reload.estimated_hours
370 assert_equal 5, first_parent.reload.estimated_hours
366 child.update_attributes(:estimated_hours => 7, :parent_issue_id => second_parent.id)
371 child.update_attributes(:estimated_hours => 7, :parent_issue_id => second_parent.id)
367 assert_equal 7, second_parent.reload.estimated_hours
372 assert_equal 7, second_parent.reload.estimated_hours
368 assert_nil first_parent.reload.estimated_hours
373 assert_nil first_parent.reload.estimated_hours
369 end
374 end
370
375
371 def test_reschuling_a_parent_should_reschedule_subtasks
376 def test_reschuling_a_parent_should_reschedule_subtasks
372 parent = Issue.generate!
377 parent = Issue.generate!
373 c1 = Issue.generate!(:start_date => '2010-05-12', :due_date => '2010-05-18', :parent_issue_id => parent.id)
378 c1 = Issue.generate!(:start_date => '2010-05-12', :due_date => '2010-05-18', :parent_issue_id => parent.id)
374 c2 = Issue.generate!(:start_date => '2010-06-03', :due_date => '2010-06-10', :parent_issue_id => parent.id)
379 c2 = Issue.generate!(:start_date => '2010-06-03', :due_date => '2010-06-10', :parent_issue_id => parent.id)
375 parent.reload
380 parent.reload
376 parent.reschedule_on!(Date.parse('2010-06-02'))
381 parent.reschedule_on!(Date.parse('2010-06-02'))
377 c1.reload
382 c1.reload
378 assert_equal [Date.parse('2010-06-02'), Date.parse('2010-06-08')], [c1.start_date, c1.due_date]
383 assert_equal [Date.parse('2010-06-02'), Date.parse('2010-06-08')], [c1.start_date, c1.due_date]
379 c2.reload
384 c2.reload
380 assert_equal [Date.parse('2010-06-03'), Date.parse('2010-06-10')], [c2.start_date, c2.due_date] # no change
385 assert_equal [Date.parse('2010-06-03'), Date.parse('2010-06-10')], [c2.start_date, c2.due_date] # no change
381 parent.reload
386 parent.reload
382 assert_equal [Date.parse('2010-06-02'), Date.parse('2010-06-10')], [parent.start_date, parent.due_date]
387 assert_equal [Date.parse('2010-06-02'), Date.parse('2010-06-10')], [parent.start_date, parent.due_date]
383 end
388 end
384
389
385 def test_project_copy_should_copy_issue_tree
390 def test_project_copy_should_copy_issue_tree
386 p = Project.create!(:name => 'Tree copy', :identifier => 'tree-copy', :tracker_ids => [1, 2])
391 p = Project.create!(:name => 'Tree copy', :identifier => 'tree-copy', :tracker_ids => [1, 2])
387 i1 = Issue.generate!(:project => p, :subject => 'i1')
392 i1 = Issue.generate!(:project => p, :subject => 'i1')
388 i2 = Issue.generate!(:project => p, :subject => 'i2', :parent_issue_id => i1.id)
393 i2 = Issue.generate!(:project => p, :subject => 'i2', :parent_issue_id => i1.id)
389 i3 = Issue.generate!(:project => p, :subject => 'i3', :parent_issue_id => i1.id)
394 i3 = Issue.generate!(:project => p, :subject => 'i3', :parent_issue_id => i1.id)
390 i4 = Issue.generate!(:project => p, :subject => 'i4', :parent_issue_id => i2.id)
395 i4 = Issue.generate!(:project => p, :subject => 'i4', :parent_issue_id => i2.id)
391 i5 = Issue.generate!(:project => p, :subject => 'i5')
396 i5 = Issue.generate!(:project => p, :subject => 'i5')
392 c = Project.new(:name => 'Copy', :identifier => 'copy', :tracker_ids => [1, 2])
397 c = Project.new(:name => 'Copy', :identifier => 'copy', :tracker_ids => [1, 2])
393 c.copy(p, :only => 'issues')
398 c.copy(p, :only => 'issues')
394 c.reload
399 c.reload
395
400
396 assert_equal 5, c.issues.count
401 assert_equal 5, c.issues.count
397 ic1, ic2, ic3, ic4, ic5 = c.issues.order('subject').all
402 ic1, ic2, ic3, ic4, ic5 = c.issues.order('subject').all
398 assert ic1.root?
403 assert ic1.root?
399 assert_equal ic1, ic2.parent
404 assert_equal ic1, ic2.parent
400 assert_equal ic1, ic3.parent
405 assert_equal ic1, ic3.parent
401 assert_equal ic2, ic4.parent
406 assert_equal ic2, ic4.parent
402 assert ic5.root?
407 assert ic5.root?
403 end
408 end
404 end
409 end
General Comments 0
You need to be logged in to leave comments. Login now