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