##// END OF EJS Templates
Deprecated Issue#move_to_project....
Jean-Philippe Lang -
r8419:165373575883
parent child
Show More
@@ -1,393 +1,393
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 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 class IssuesController < ApplicationController
19 19 menu_item :new_issue, :only => [:new, :create]
20 20 default_search_scope :issues
21 21
22 22 before_filter :find_issue, :only => [:show, :edit, :update]
23 23 before_filter :find_issues, :only => [:bulk_edit, :bulk_update, :move, :perform_move, :destroy]
24 24 before_filter :check_project_uniqueness, :only => [:move, :perform_move]
25 25 before_filter :find_project, :only => [:new, :create]
26 26 before_filter :authorize, :except => [:index]
27 27 before_filter :find_optional_project, :only => [:index]
28 28 before_filter :check_for_default_issue_status, :only => [:new, :create]
29 29 before_filter :build_new_issue_from_params, :only => [:new, :create]
30 30 accept_rss_auth :index, :show
31 31 accept_api_auth :index, :show, :create, :update, :destroy
32 32
33 33 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
34 34
35 35 helper :journals
36 36 helper :projects
37 37 include ProjectsHelper
38 38 helper :custom_fields
39 39 include CustomFieldsHelper
40 40 helper :issue_relations
41 41 include IssueRelationsHelper
42 42 helper :watchers
43 43 include WatchersHelper
44 44 helper :attachments
45 45 include AttachmentsHelper
46 46 helper :queries
47 47 include QueriesHelper
48 48 helper :repositories
49 49 include RepositoriesHelper
50 50 helper :sort
51 51 include SortHelper
52 52 include IssuesHelper
53 53 helper :timelog
54 54 helper :gantt
55 55 include Redmine::Export::PDF
56 56
57 57 verify :method => :post, :only => :create, :render => {:nothing => true, :status => :method_not_allowed }
58 58 verify :method => :post, :only => :bulk_update, :render => {:nothing => true, :status => :method_not_allowed }
59 59 verify :method => :put, :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
60 60
61 61 def index
62 62 retrieve_query
63 63 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
64 64 sort_update(@query.sortable_columns)
65 65
66 66 if @query.valid?
67 67 case params[:format]
68 68 when 'csv', 'pdf'
69 69 @limit = Setting.issues_export_limit.to_i
70 70 when 'atom'
71 71 @limit = Setting.feeds_limit.to_i
72 72 when 'xml', 'json'
73 73 @offset, @limit = api_offset_and_limit
74 74 else
75 75 @limit = per_page_option
76 76 end
77 77
78 78 @issue_count = @query.issue_count
79 79 @issue_pages = Paginator.new self, @issue_count, @limit, params['page']
80 80 @offset ||= @issue_pages.current.offset
81 81 @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
82 82 :order => sort_clause,
83 83 :offset => @offset,
84 84 :limit => @limit)
85 85 @issue_count_by_group = @query.issue_count_by_group
86 86
87 87 respond_to do |format|
88 88 format.html { render :template => 'issues/index', :layout => !request.xhr? }
89 89 format.api {
90 90 Issue.load_relations(@issues) if include_in_api_response?('relations')
91 91 }
92 92 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
93 93 format.csv { send_data(issues_to_csv(@issues, @project, @query, params), :type => 'text/csv; header=present', :filename => 'export.csv') }
94 94 format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'export.pdf') }
95 95 end
96 96 else
97 97 respond_to do |format|
98 98 format.html { render(:template => 'issues/index', :layout => !request.xhr?) }
99 99 format.any(:atom, :csv, :pdf) { render(:nothing => true) }
100 100 format.api { render_validation_errors(@query) }
101 101 end
102 102 end
103 103 rescue ActiveRecord::RecordNotFound
104 104 render_404
105 105 end
106 106
107 107 def show
108 108 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
109 109 @journals.each_with_index {|j,i| j.indice = i+1}
110 110 @journals.reverse! if User.current.wants_comments_in_reverse_order?
111 111
112 112 if User.current.allowed_to?(:view_changesets, @project)
113 113 @changesets = @issue.changesets.visible.all
114 114 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
115 115 end
116 116
117 117 @relations = @issue.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
118 118 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
119 119 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
120 120 @priorities = IssuePriority.active
121 121 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
122 122 respond_to do |format|
123 123 format.html {
124 124 retrieve_previous_and_next_issue_ids
125 125 render :template => 'issues/show'
126 126 }
127 127 format.api
128 128 format.atom { render :template => 'journals/index', :layout => false, :content_type => 'application/atom+xml' }
129 129 format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
130 130 end
131 131 end
132 132
133 133 # Add a new issue
134 134 # The new issue will be created from an existing one if copy_from parameter is given
135 135 def new
136 136 respond_to do |format|
137 137 format.html { render :action => 'new', :layout => !request.xhr? }
138 138 format.js {
139 139 render(:update) { |page|
140 140 if params[:project_change]
141 141 page.replace_html 'all_attributes', :partial => 'form'
142 142 else
143 143 page.replace_html 'attributes', :partial => 'attributes'
144 144 end
145 145 m = User.current.allowed_to?(:log_time, @issue.project) ? 'show' : 'hide'
146 146 page << "if ($('log_time')) {Element.#{m}('log_time');}"
147 147 }
148 148 }
149 149 end
150 150 end
151 151
152 152 def create
153 153 call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
154 154 if @issue.save
155 155 attachments = Attachment.attach_files(@issue, params[:attachments])
156 156 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
157 157 respond_to do |format|
158 158 format.html {
159 159 render_attachment_warning_if_needed(@issue)
160 160 flash[:notice] = l(:notice_issue_successful_create, :id => "<a href='#{issue_path(@issue)}'>##{@issue.id}</a>")
161 161 redirect_to(params[:continue] ? { :action => 'new', :project_id => @project, :issue => {:tracker_id => @issue.tracker, :parent_issue_id => @issue.parent_issue_id}.reject {|k,v| v.nil?} } :
162 162 { :action => 'show', :id => @issue })
163 163 }
164 164 format.api { render :action => 'show', :status => :created, :location => issue_url(@issue) }
165 165 end
166 166 return
167 167 else
168 168 respond_to do |format|
169 169 format.html { render :action => 'new' }
170 170 format.api { render_validation_errors(@issue) }
171 171 end
172 172 end
173 173 end
174 174
175 175 def edit
176 176 update_issue_from_params
177 177
178 178 @journal = @issue.current_journal
179 179
180 180 respond_to do |format|
181 181 format.html { }
182 182 format.xml { }
183 183 end
184 184 end
185 185
186 186 def update
187 187 update_issue_from_params
188 188
189 189 if @issue.save_issue_with_child_records(params, @time_entry)
190 190 render_attachment_warning_if_needed(@issue)
191 191 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
192 192
193 193 respond_to do |format|
194 194 format.html { redirect_back_or_default({:action => 'show', :id => @issue}) }
195 195 format.api { head :ok }
196 196 end
197 197 else
198 198 render_attachment_warning_if_needed(@issue)
199 199 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
200 200 @journal = @issue.current_journal
201 201
202 202 respond_to do |format|
203 203 format.html { render :action => 'edit' }
204 204 format.api { render_validation_errors(@issue) }
205 205 end
206 206 end
207 207 end
208 208
209 209 # Bulk edit/copy a set of issues
210 210 def bulk_edit
211 211 @issues.sort!
212 212 @copy = params[:copy].present?
213 213 @notes = params[:notes]
214 214
215 215 if User.current.allowed_to?(:move_issues, @projects)
216 216 @allowed_projects = Issue.allowed_target_projects_on_move
217 217 if params[:issue]
218 218 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:issue][:project_id]}
219 219 if @target_project
220 220 target_projects = [@target_project]
221 221 end
222 222 end
223 223 end
224 224 target_projects ||= @projects
225 225
226 226 @available_statuses = target_projects.map{|p|Workflow.available_statuses(p)}.inject{|memo,w|memo & w}
227 227 @custom_fields = target_projects.map{|p|p.all_issue_custom_fields}.inject{|memo,c|memo & c}
228 228 @assignables = target_projects.map(&:assignable_users).inject{|memo,a| memo & a}
229 229 @trackers = target_projects.map(&:trackers).inject{|memo,t| memo & t}
230 230
231 231 render :layout => false if request.xhr?
232 232 end
233 233
234 234 def bulk_update
235 235 @issues.sort!
236 236 @copy = params[:copy].present?
237 237 attributes = parse_params_for_bulk_issue_attributes(params)
238 238
239 239 unsaved_issue_ids = []
240 240 moved_issues = []
241 241 @issues.each do |issue|
242 242 issue.reload
243 243 if @copy
244 issue = Issue.new.copy_from(issue)
244 issue = issue.copy
245 245 end
246 246 journal = issue.init_journal(User.current, params[:notes])
247 247 issue.safe_attributes = attributes
248 248 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
249 249 if issue.save
250 250 moved_issues << issue
251 251 else
252 252 # Keep unsaved issue ids to display them in flash error
253 253 unsaved_issue_ids << issue.id
254 254 end
255 255 end
256 256 set_flash_from_bulk_issue_save(@issues, unsaved_issue_ids)
257 257
258 258 if params[:follow]
259 259 if @issues.size == 1 && moved_issues.size == 1
260 260 redirect_to :controller => 'issues', :action => 'show', :id => moved_issues.first
261 261 elsif moved_issues.map(&:project).uniq.size == 1
262 262 redirect_to :controller => 'issues', :action => 'index', :project_id => moved_issues.map(&:project).first
263 263 end
264 264 else
265 265 redirect_back_or_default({:controller => 'issues', :action => 'index', :project_id => @project})
266 266 end
267 267 end
268 268
269 269 verify :method => :delete, :only => :destroy, :render => { :nothing => true, :status => :method_not_allowed }
270 270 def destroy
271 271 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
272 272 if @hours > 0
273 273 case params[:todo]
274 274 when 'destroy'
275 275 # nothing to do
276 276 when 'nullify'
277 277 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
278 278 when 'reassign'
279 279 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
280 280 if reassign_to.nil?
281 281 flash.now[:error] = l(:error_issue_not_found_in_project)
282 282 return
283 283 else
284 284 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
285 285 end
286 286 else
287 287 # display the destroy form if it's a user request
288 288 return unless api_request?
289 289 end
290 290 end
291 291 @issues.each do |issue|
292 292 begin
293 293 issue.reload.destroy
294 294 rescue ::ActiveRecord::RecordNotFound # raised by #reload if issue no longer exists
295 295 # nothing to do, issue was already deleted (eg. by a parent)
296 296 end
297 297 end
298 298 respond_to do |format|
299 299 format.html { redirect_back_or_default(:action => 'index', :project_id => @project) }
300 300 format.api { head :ok }
301 301 end
302 302 end
303 303
304 304 private
305 305 def find_issue
306 306 # Issue.visible.find(...) can not be used to redirect user to the login form
307 307 # if the issue actually exists but requires authentication
308 308 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
309 309 unless @issue.visible?
310 310 deny_access
311 311 return
312 312 end
313 313 @project = @issue.project
314 314 rescue ActiveRecord::RecordNotFound
315 315 render_404
316 316 end
317 317
318 318 def find_project
319 319 project_id = params[:project_id] || (params[:issue] && params[:issue][:project_id])
320 320 @project = Project.find(project_id)
321 321 rescue ActiveRecord::RecordNotFound
322 322 render_404
323 323 end
324 324
325 325 def retrieve_previous_and_next_issue_ids
326 326 retrieve_query_from_session
327 327 if @query
328 328 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
329 329 sort_update(@query.sortable_columns, 'issues_index_sort')
330 330 limit = 500
331 331 issue_ids = @query.issue_ids(:order => sort_clause, :limit => (limit + 1))
332 332 if (idx = issue_ids.index(@issue.id)) && idx < limit
333 333 @prev_issue_id = issue_ids[idx - 1] if idx > 0
334 334 @next_issue_id = issue_ids[idx + 1] if idx < (issue_ids.size - 1)
335 335 end
336 336 end
337 337 end
338 338
339 339 # Used by #edit and #update to set some common instance variables
340 340 # from the params
341 341 # TODO: Refactor, not everything in here is needed by #edit
342 342 def update_issue_from_params
343 343 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
344 344 @priorities = IssuePriority.active
345 345 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
346 346 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
347 347 @time_entry.attributes = params[:time_entry]
348 348
349 349 @notes = params[:notes] || (params[:issue].present? ? params[:issue][:notes] : nil)
350 350 @issue.init_journal(User.current, @notes)
351 351 @issue.safe_attributes = params[:issue]
352 352 end
353 353
354 354 # TODO: Refactor, lots of extra code in here
355 355 # TODO: Changing tracker on an existing issue should not trigger this
356 356 def build_new_issue_from_params
357 357 if params[:id].blank?
358 358 @issue = Issue.new
359 359 @issue.copy_from(params[:copy_from]) if params[:copy_from]
360 360 @issue.project = @project
361 361 else
362 362 @issue = @project.issues.visible.find(params[:id])
363 363 end
364 364
365 365 @issue.project = @project
366 366 @issue.author = User.current
367 367 # Tracker must be set before custom field values
368 368 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
369 369 if @issue.tracker.nil?
370 370 render_error l(:error_no_tracker_in_project)
371 371 return false
372 372 end
373 373 @issue.start_date ||= Date.today if Setting.default_issue_start_date_to_creation_date?
374 374 @issue.safe_attributes = params[:issue]
375 375
376 376 @priorities = IssuePriority.active
377 377 @allowed_statuses = @issue.new_statuses_allowed_to(User.current, true)
378 378 end
379 379
380 380 def check_for_default_issue_status
381 381 if IssueStatus.default.nil?
382 382 render_error l(:error_no_default_issue_status)
383 383 return false
384 384 end
385 385 end
386 386
387 387 def parse_params_for_bulk_issue_attributes(params)
388 388 attributes = (params[:issue] || {}).reject {|k,v| v.blank?}
389 389 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
390 390 attributes[:custom_field_values].reject! {|k,v| v.blank?} if attributes[:custom_field_values]
391 391 attributes
392 392 end
393 393 end
@@ -1,1010 +1,1019
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 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 class Issue < ActiveRecord::Base
19 19 include Redmine::SafeAttributes
20 20
21 21 belongs_to :project
22 22 belongs_to :tracker
23 23 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
24 24 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
25 25 belongs_to :assigned_to, :class_name => 'Principal', :foreign_key => 'assigned_to_id'
26 26 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
27 27 belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
28 28 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
29 29
30 30 has_many :journals, :as => :journalized, :dependent => :destroy
31 31 has_many :time_entries, :dependent => :delete_all
32 32 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
33 33
34 34 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
35 35 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
36 36
37 37 acts_as_nested_set :scope => 'root_id', :dependent => :destroy
38 38 acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed
39 39 acts_as_customizable
40 40 acts_as_watchable
41 41 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
42 42 :include => [:project, :journals],
43 43 # sort by id so that limited eager loading doesn't break with postgresql
44 44 :order_column => "#{table_name}.id"
45 45 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
46 46 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
47 47 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
48 48
49 49 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
50 50 :author_key => :author_id
51 51
52 52 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
53 53
54 54 attr_reader :current_journal
55 55
56 56 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
57 57
58 58 validates_length_of :subject, :maximum => 255
59 59 validates_inclusion_of :done_ratio, :in => 0..100
60 60 validates_numericality_of :estimated_hours, :allow_nil => true
61 61 validate :validate_issue
62 62
63 63 named_scope :visible, lambda {|*args| { :include => :project,
64 64 :conditions => Issue.visible_condition(args.shift || User.current, *args) } }
65 65
66 66 named_scope :open, lambda {|*args|
67 67 is_closed = args.size > 0 ? !args.first : false
68 68 {:conditions => ["#{IssueStatus.table_name}.is_closed = ?", is_closed], :include => :status}
69 69 }
70 70
71 71 named_scope :recently_updated, :order => "#{Issue.table_name}.updated_on DESC"
72 72 named_scope :with_limit, lambda { |limit| { :limit => limit} }
73 73 named_scope :on_active_project, :include => [:status, :project, :tracker],
74 74 :conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"]
75 75
76 76 before_create :default_assign
77 77 before_save :close_duplicates, :update_done_ratio_from_issue_status
78 78 after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?}
79 79 after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal
80 80 after_destroy :update_parent_attributes
81 81
82 82 # Returns a SQL conditions string used to find all issues visible by the specified user
83 83 def self.visible_condition(user, options={})
84 84 Project.allowed_to_condition(user, :view_issues, options) do |role, user|
85 85 case role.issues_visibility
86 86 when 'all'
87 87 nil
88 88 when 'default'
89 89 user_ids = [user.id] + user.groups.map(&:id)
90 90 "(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
91 91 when 'own'
92 92 user_ids = [user.id] + user.groups.map(&:id)
93 93 "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
94 94 else
95 95 '1=0'
96 96 end
97 97 end
98 98 end
99 99
100 100 # Returns true if usr or current user is allowed to view the issue
101 101 def visible?(usr=nil)
102 102 (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
103 103 case role.issues_visibility
104 104 when 'all'
105 105 true
106 106 when 'default'
107 107 !self.is_private? || self.author == user || user.is_or_belongs_to?(assigned_to)
108 108 when 'own'
109 109 self.author == user || user.is_or_belongs_to?(assigned_to)
110 110 else
111 111 false
112 112 end
113 113 end
114 114 end
115 115
116 116 def initialize(attributes=nil, *args)
117 117 super
118 118 if new_record?
119 119 # set default values for new records only
120 120 self.status ||= IssueStatus.default
121 121 self.priority ||= IssuePriority.default
122 122 end
123 123 end
124 124
125 125 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
126 126 def available_custom_fields
127 127 (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields.all) : []
128 128 end
129 129
130 130 def copy_from(arg)
131 131 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
132 132 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
133 133 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
134 134 self.status = issue.status
135 135 self.author = User.current
136 136 self
137 137 end
138 138
139 # Returns an unsaved copy of the issue
140 def copy(attributes=nil)
141 copy = self.class.new.copy_from(self)
142 copy.attributes = attributes if attributes
143 copy
144 end
145
139 146 # Moves/copies an issue to a new project and tracker
140 147 # Returns the moved/copied issue on success, false on failure
141 148 def move_to_project(new_project, new_tracker=nil, options={})
149 ActiveSupport::Deprecation.warn "Issue#move_to_project is deprecated, use #project= instead."
150
142 151 if options[:copy]
143 issue = self.class.new.copy_from(self)
152 issue = self.copy
144 153 else
145 154 issue = self
146 155 end
147 156
148 157 issue.init_journal(User.current, options[:notes])
149 158
150 159 # Preserve previous behaviour
151 160 # #move_to_project doesn't change tracker automatically
152 161 issue.send :project=, new_project, true
153 162 if new_tracker
154 163 issue.tracker = new_tracker
155 164 end
156 165 # Allow bulk setting of attributes on the issue
157 166 if options[:attributes]
158 167 issue.attributes = options[:attributes]
159 168 end
160 169
161 170 issue.save ? issue : false
162 171 end
163 172
164 173 def status_id=(sid)
165 174 self.status = nil
166 175 write_attribute(:status_id, sid)
167 176 end
168 177
169 178 def priority_id=(pid)
170 179 self.priority = nil
171 180 write_attribute(:priority_id, pid)
172 181 end
173 182
174 183 def category_id=(cid)
175 184 self.category = nil
176 185 write_attribute(:category_id, cid)
177 186 end
178 187
179 188 def fixed_version_id=(vid)
180 189 self.fixed_version = nil
181 190 write_attribute(:fixed_version_id, vid)
182 191 end
183 192
184 193 def tracker_id=(tid)
185 194 self.tracker = nil
186 195 result = write_attribute(:tracker_id, tid)
187 196 @custom_field_values = nil
188 197 result
189 198 end
190 199
191 200 def project_id=(project_id)
192 201 if project_id.to_s != self.project_id.to_s
193 202 self.project = (project_id.present? ? Project.find_by_id(project_id) : nil)
194 203 end
195 204 end
196 205
197 206 def project=(project, keep_tracker=false)
198 207 project_was = self.project
199 208 write_attribute(:project_id, project ? project.id : nil)
200 209 association_instance_set('project', project)
201 210 if project_was && project && project_was != project
202 211 unless keep_tracker || project.trackers.include?(tracker)
203 212 self.tracker = project.trackers.first
204 213 end
205 214 # Reassign to the category with same name if any
206 215 if category
207 216 self.category = project.issue_categories.find_by_name(category.name)
208 217 end
209 218 # Keep the fixed_version if it's still valid in the new_project
210 219 if fixed_version && fixed_version.project != project && !project.shared_versions.include?(fixed_version)
211 220 self.fixed_version = nil
212 221 end
213 222 if parent && parent.project_id != project_id
214 223 self.parent_issue_id = nil
215 224 end
216 225 @custom_field_values = nil
217 226 end
218 227 end
219 228
220 229 def description=(arg)
221 230 if arg.is_a?(String)
222 231 arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n")
223 232 end
224 233 write_attribute(:description, arg)
225 234 end
226 235
227 236 # Overrides attributes= so that project and tracker get assigned first
228 237 def attributes_with_project_and_tracker_first=(new_attributes, *args)
229 238 return if new_attributes.nil?
230 239 attrs = new_attributes.dup
231 240 attrs.stringify_keys!
232 241
233 242 %w(project project_id tracker tracker_id).each do |attr|
234 243 if attrs.has_key?(attr)
235 244 send "#{attr}=", attrs.delete(attr)
236 245 end
237 246 end
238 247 send :attributes_without_project_and_tracker_first=, attrs, *args
239 248 end
240 249 # Do not redefine alias chain on reload (see #4838)
241 250 alias_method_chain(:attributes=, :project_and_tracker_first) unless method_defined?(:attributes_without_project_and_tracker_first=)
242 251
243 252 def estimated_hours=(h)
244 253 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
245 254 end
246 255
247 256 safe_attributes 'project_id',
248 257 :if => lambda {|issue, user|
249 258 if user.allowed_to?(:move_issues, issue.project)
250 259 projects = Issue.allowed_target_projects_on_move(user)
251 260 projects.include?(issue.project) && projects.size > 1
252 261 end
253 262 }
254 263
255 264 safe_attributes 'tracker_id',
256 265 'status_id',
257 266 'category_id',
258 267 'assigned_to_id',
259 268 'priority_id',
260 269 'fixed_version_id',
261 270 'subject',
262 271 'description',
263 272 'start_date',
264 273 'due_date',
265 274 'done_ratio',
266 275 'estimated_hours',
267 276 'custom_field_values',
268 277 'custom_fields',
269 278 'lock_version',
270 279 :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
271 280
272 281 safe_attributes 'status_id',
273 282 'assigned_to_id',
274 283 'fixed_version_id',
275 284 'done_ratio',
276 285 'lock_version',
277 286 :if => lambda {|issue, user| issue.new_statuses_allowed_to(user).any? }
278 287
279 288 safe_attributes 'watcher_user_ids',
280 289 :if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)}
281 290
282 291 safe_attributes 'is_private',
283 292 :if => lambda {|issue, user|
284 293 user.allowed_to?(:set_issues_private, issue.project) ||
285 294 (issue.author == user && user.allowed_to?(:set_own_issues_private, issue.project))
286 295 }
287 296
288 297 safe_attributes 'parent_issue_id',
289 298 :if => lambda {|issue, user| (issue.new_record? || user.allowed_to?(:edit_issues, issue.project)) &&
290 299 user.allowed_to?(:manage_subtasks, issue.project)}
291 300
292 301 # Safely sets attributes
293 302 # Should be called from controllers instead of #attributes=
294 303 # attr_accessible is too rough because we still want things like
295 304 # Issue.new(:project => foo) to work
296 305 # TODO: move workflow/permission checks from controllers to here
297 306 def safe_attributes=(attrs, user=User.current)
298 307 return unless attrs.is_a?(Hash)
299 308
300 309 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
301 310 attrs = delete_unsafe_attributes(attrs, user)
302 311 return if attrs.empty?
303 312
304 313 # Project and Tracker must be set before since new_statuses_allowed_to depends on it.
305 314 if p = attrs.delete('project_id')
306 315 self.project_id = p
307 316 end
308 317
309 318 if t = attrs.delete('tracker_id')
310 319 self.tracker_id = t
311 320 end
312 321
313 322 if attrs['status_id']
314 323 unless new_statuses_allowed_to(user).collect(&:id).include?(attrs['status_id'].to_i)
315 324 attrs.delete('status_id')
316 325 end
317 326 end
318 327
319 328 unless leaf?
320 329 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
321 330 end
322 331
323 332 if attrs['parent_issue_id'].present?
324 333 attrs.delete('parent_issue_id') unless Issue.visible(user).exists?(attrs['parent_issue_id'].to_i)
325 334 end
326 335
327 336 # mass-assignment security bypass
328 337 self.send :attributes=, attrs, false
329 338 end
330 339
331 340 def done_ratio
332 341 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
333 342 status.default_done_ratio
334 343 else
335 344 read_attribute(:done_ratio)
336 345 end
337 346 end
338 347
339 348 def self.use_status_for_done_ratio?
340 349 Setting.issue_done_ratio == 'issue_status'
341 350 end
342 351
343 352 def self.use_field_for_done_ratio?
344 353 Setting.issue_done_ratio == 'issue_field'
345 354 end
346 355
347 356 def validate_issue
348 357 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
349 358 errors.add :due_date, :not_a_date
350 359 end
351 360
352 361 if self.due_date and self.start_date and self.due_date < self.start_date
353 362 errors.add :due_date, :greater_than_start_date
354 363 end
355 364
356 365 if start_date && soonest_start && start_date < soonest_start
357 366 errors.add :start_date, :invalid
358 367 end
359 368
360 369 if fixed_version
361 370 if !assignable_versions.include?(fixed_version)
362 371 errors.add :fixed_version_id, :inclusion
363 372 elsif reopened? && fixed_version.closed?
364 373 errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version)
365 374 end
366 375 end
367 376
368 377 # Checks that the issue can not be added/moved to a disabled tracker
369 378 if project && (tracker_id_changed? || project_id_changed?)
370 379 unless project.trackers.include?(tracker)
371 380 errors.add :tracker_id, :inclusion
372 381 end
373 382 end
374 383
375 384 # Checks parent issue assignment
376 385 if @parent_issue
377 386 if @parent_issue.project_id != project_id
378 387 errors.add :parent_issue_id, :not_same_project
379 388 elsif !new_record?
380 389 # moving an existing issue
381 390 if @parent_issue.root_id != root_id
382 391 # we can always move to another tree
383 392 elsif move_possible?(@parent_issue)
384 393 # move accepted inside tree
385 394 else
386 395 errors.add :parent_issue_id, :not_a_valid_parent
387 396 end
388 397 end
389 398 end
390 399 end
391 400
392 401 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
393 402 # even if the user turns off the setting later
394 403 def update_done_ratio_from_issue_status
395 404 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
396 405 self.done_ratio = status.default_done_ratio
397 406 end
398 407 end
399 408
400 409 def init_journal(user, notes = "")
401 410 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
402 411 if new_record?
403 412 @current_journal.notify = false
404 413 else
405 414 @attributes_before_change = attributes.dup
406 415 @custom_values_before_change = {}
407 416 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
408 417 end
409 418 # Make sure updated_on is updated when adding a note.
410 419 updated_on_will_change!
411 420 @current_journal
412 421 end
413 422
414 423 # Return true if the issue is closed, otherwise false
415 424 def closed?
416 425 self.status.is_closed?
417 426 end
418 427
419 428 # Return true if the issue is being reopened
420 429 def reopened?
421 430 if !new_record? && status_id_changed?
422 431 status_was = IssueStatus.find_by_id(status_id_was)
423 432 status_new = IssueStatus.find_by_id(status_id)
424 433 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
425 434 return true
426 435 end
427 436 end
428 437 false
429 438 end
430 439
431 440 # Return true if the issue is being closed
432 441 def closing?
433 442 if !new_record? && status_id_changed?
434 443 status_was = IssueStatus.find_by_id(status_id_was)
435 444 status_new = IssueStatus.find_by_id(status_id)
436 445 if status_was && status_new && !status_was.is_closed? && status_new.is_closed?
437 446 return true
438 447 end
439 448 end
440 449 false
441 450 end
442 451
443 452 # Returns true if the issue is overdue
444 453 def overdue?
445 454 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
446 455 end
447 456
448 457 # Is the amount of work done less than it should for the due date
449 458 def behind_schedule?
450 459 return false if start_date.nil? || due_date.nil?
451 460 done_date = start_date + ((due_date - start_date+1)* done_ratio/100).floor
452 461 return done_date <= Date.today
453 462 end
454 463
455 464 # Does this issue have children?
456 465 def children?
457 466 !leaf?
458 467 end
459 468
460 469 # Users the issue can be assigned to
461 470 def assignable_users
462 471 users = project.assignable_users
463 472 users << author if author
464 473 users << assigned_to if assigned_to
465 474 users.uniq.sort
466 475 end
467 476
468 477 # Versions that the issue can be assigned to
469 478 def assignable_versions
470 479 @assignable_versions ||= (project.shared_versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort
471 480 end
472 481
473 482 # Returns true if this issue is blocked by another issue that is still open
474 483 def blocked?
475 484 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
476 485 end
477 486
478 487 # Returns an array of status that user is able to apply
479 488 def new_statuses_allowed_to(user, include_default=false)
480 489 statuses = status.find_new_statuses_allowed_to(
481 490 user.roles_for_project(project),
482 491 tracker,
483 492 author == user,
484 493 assigned_to_id_changed? ? assigned_to_id_was == user.id : assigned_to_id == user.id
485 494 )
486 495 statuses << status unless statuses.empty?
487 496 statuses << IssueStatus.default if include_default
488 497 statuses = statuses.uniq.sort
489 498 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
490 499 end
491 500
492 501 # Returns the mail adresses of users that should be notified
493 502 def recipients
494 503 notified = project.notified_users
495 504 # Author and assignee are always notified unless they have been
496 505 # locked or don't want to be notified
497 506 notified << author if author && author.active? && author.notify_about?(self)
498 507 if assigned_to
499 508 if assigned_to.is_a?(Group)
500 509 notified += assigned_to.users.select {|u| u.active? && u.notify_about?(self)}
501 510 else
502 511 notified << assigned_to if assigned_to.active? && assigned_to.notify_about?(self)
503 512 end
504 513 end
505 514 notified.uniq!
506 515 # Remove users that can not view the issue
507 516 notified.reject! {|user| !visible?(user)}
508 517 notified.collect(&:mail)
509 518 end
510 519
511 520 # Returns the number of hours spent on this issue
512 521 def spent_hours
513 522 @spent_hours ||= time_entries.sum(:hours) || 0
514 523 end
515 524
516 525 # Returns the total number of hours spent on this issue and its descendants
517 526 #
518 527 # Example:
519 528 # spent_hours => 0.0
520 529 # spent_hours => 50.2
521 530 def total_spent_hours
522 531 @total_spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours",
523 532 :joins => "LEFT JOIN #{TimeEntry.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id").to_f || 0.0
524 533 end
525 534
526 535 def relations
527 536 @relations ||= (relations_from + relations_to).sort
528 537 end
529 538
530 539 # Preloads relations for a collection of issues
531 540 def self.load_relations(issues)
532 541 if issues.any?
533 542 relations = IssueRelation.all(:conditions => ["issue_from_id IN (:ids) OR issue_to_id IN (:ids)", {:ids => issues.map(&:id)}])
534 543 issues.each do |issue|
535 544 issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
536 545 end
537 546 end
538 547 end
539 548
540 549 # Preloads visible spent time for a collection of issues
541 550 def self.load_visible_spent_hours(issues, user=User.current)
542 551 if issues.any?
543 552 hours_by_issue_id = TimeEntry.visible(user).sum(:hours, :group => :issue_id)
544 553 issues.each do |issue|
545 554 issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0)
546 555 end
547 556 end
548 557 end
549 558
550 559 # Finds an issue relation given its id.
551 560 def find_relation(relation_id)
552 561 IssueRelation.find(relation_id, :conditions => ["issue_to_id = ? OR issue_from_id = ?", id, id])
553 562 end
554 563
555 564 def all_dependent_issues(except=[])
556 565 except << self
557 566 dependencies = []
558 567 relations_from.each do |relation|
559 568 if relation.issue_to && !except.include?(relation.issue_to)
560 569 dependencies << relation.issue_to
561 570 dependencies += relation.issue_to.all_dependent_issues(except)
562 571 end
563 572 end
564 573 dependencies
565 574 end
566 575
567 576 # Returns an array of issues that duplicate this one
568 577 def duplicates
569 578 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
570 579 end
571 580
572 581 # Returns the due date or the target due date if any
573 582 # Used on gantt chart
574 583 def due_before
575 584 due_date || (fixed_version ? fixed_version.effective_date : nil)
576 585 end
577 586
578 587 # Returns the time scheduled for this issue.
579 588 #
580 589 # Example:
581 590 # Start Date: 2/26/09, End Date: 3/04/09
582 591 # duration => 6
583 592 def duration
584 593 (start_date && due_date) ? due_date - start_date : 0
585 594 end
586 595
587 596 def soonest_start
588 597 @soonest_start ||= (
589 598 relations_to.collect{|relation| relation.successor_soonest_start} +
590 599 ancestors.collect(&:soonest_start)
591 600 ).compact.max
592 601 end
593 602
594 603 def reschedule_after(date)
595 604 return if date.nil?
596 605 if leaf?
597 606 if start_date.nil? || start_date < date
598 607 self.start_date, self.due_date = date, date + duration
599 608 save
600 609 end
601 610 else
602 611 leaves.each do |leaf|
603 612 leaf.reschedule_after(date)
604 613 end
605 614 end
606 615 end
607 616
608 617 def <=>(issue)
609 618 if issue.nil?
610 619 -1
611 620 elsif root_id != issue.root_id
612 621 (root_id || 0) <=> (issue.root_id || 0)
613 622 else
614 623 (lft || 0) <=> (issue.lft || 0)
615 624 end
616 625 end
617 626
618 627 def to_s
619 628 "#{tracker} ##{id}: #{subject}"
620 629 end
621 630
622 631 # Returns a string of css classes that apply to the issue
623 632 def css_classes
624 633 s = "issue status-#{status.position} priority-#{priority.position}"
625 634 s << ' closed' if closed?
626 635 s << ' overdue' if overdue?
627 636 s << ' child' if child?
628 637 s << ' parent' unless leaf?
629 638 s << ' private' if is_private?
630 639 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
631 640 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
632 641 s
633 642 end
634 643
635 644 # Saves an issue, time_entry, attachments, and a journal from the parameters
636 645 # Returns false if save fails
637 646 def save_issue_with_child_records(params, existing_time_entry=nil)
638 647 Issue.transaction do
639 648 if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, project)
640 649 @time_entry = existing_time_entry || TimeEntry.new
641 650 @time_entry.project = project
642 651 @time_entry.issue = self
643 652 @time_entry.user = User.current
644 653 @time_entry.spent_on = User.current.today
645 654 @time_entry.attributes = params[:time_entry]
646 655 self.time_entries << @time_entry
647 656 end
648 657
649 658 if valid?
650 659 attachments = Attachment.attach_files(self, params[:attachments])
651 660 # TODO: Rename hook
652 661 Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
653 662 begin
654 663 if save
655 664 # TODO: Rename hook
656 665 Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
657 666 else
658 667 raise ActiveRecord::Rollback
659 668 end
660 669 rescue ActiveRecord::StaleObjectError
661 670 attachments[:files].each(&:destroy)
662 671 errors.add :base, l(:notice_locking_conflict)
663 672 raise ActiveRecord::Rollback
664 673 end
665 674 end
666 675 end
667 676 end
668 677
669 678 # Unassigns issues from +version+ if it's no longer shared with issue's project
670 679 def self.update_versions_from_sharing_change(version)
671 680 # Update issues assigned to the version
672 681 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
673 682 end
674 683
675 684 # Unassigns issues from versions that are no longer shared
676 685 # after +project+ was moved
677 686 def self.update_versions_from_hierarchy_change(project)
678 687 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
679 688 # Update issues of the moved projects and issues assigned to a version of a moved project
680 689 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
681 690 end
682 691
683 692 def parent_issue_id=(arg)
684 693 parent_issue_id = arg.blank? ? nil : arg.to_i
685 694 if parent_issue_id && @parent_issue = Issue.find_by_id(parent_issue_id)
686 695 @parent_issue.id
687 696 else
688 697 @parent_issue = nil
689 698 nil
690 699 end
691 700 end
692 701
693 702 def parent_issue_id
694 703 if instance_variable_defined? :@parent_issue
695 704 @parent_issue.nil? ? nil : @parent_issue.id
696 705 else
697 706 parent_id
698 707 end
699 708 end
700 709
701 710 # Extracted from the ReportsController.
702 711 def self.by_tracker(project)
703 712 count_and_group_by(:project => project,
704 713 :field => 'tracker_id',
705 714 :joins => Tracker.table_name)
706 715 end
707 716
708 717 def self.by_version(project)
709 718 count_and_group_by(:project => project,
710 719 :field => 'fixed_version_id',
711 720 :joins => Version.table_name)
712 721 end
713 722
714 723 def self.by_priority(project)
715 724 count_and_group_by(:project => project,
716 725 :field => 'priority_id',
717 726 :joins => IssuePriority.table_name)
718 727 end
719 728
720 729 def self.by_category(project)
721 730 count_and_group_by(:project => project,
722 731 :field => 'category_id',
723 732 :joins => IssueCategory.table_name)
724 733 end
725 734
726 735 def self.by_assigned_to(project)
727 736 count_and_group_by(:project => project,
728 737 :field => 'assigned_to_id',
729 738 :joins => User.table_name)
730 739 end
731 740
732 741 def self.by_author(project)
733 742 count_and_group_by(:project => project,
734 743 :field => 'author_id',
735 744 :joins => User.table_name)
736 745 end
737 746
738 747 def self.by_subproject(project)
739 748 ActiveRecord::Base.connection.select_all("select s.id as status_id,
740 749 s.is_closed as closed,
741 750 #{Issue.table_name}.project_id as project_id,
742 751 count(#{Issue.table_name}.id) as total
743 752 from
744 753 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s
745 754 where
746 755 #{Issue.table_name}.status_id=s.id
747 756 and #{Issue.table_name}.project_id = #{Project.table_name}.id
748 757 and #{visible_condition(User.current, :project => project, :with_subprojects => true)}
749 758 and #{Issue.table_name}.project_id <> #{project.id}
750 759 group by s.id, s.is_closed, #{Issue.table_name}.project_id") if project.descendants.active.any?
751 760 end
752 761 # End ReportsController extraction
753 762
754 763 # Returns an array of projects that current user can move issues to
755 764 def self.allowed_target_projects_on_move(user=User.current)
756 765 projects = []
757 766 if user.admin?
758 767 # admin is allowed to move issues to any active (visible) project
759 768 projects = Project.visible(user).all
760 769 elsif user.logged?
761 770 if Role.non_member.allowed_to?(:move_issues)
762 771 projects = Project.visible(user).all
763 772 else
764 773 user.memberships.each {|m| projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}}
765 774 end
766 775 end
767 776 projects
768 777 end
769 778
770 779 private
771 780
772 781 def after_project_change
773 782 # Update project_id on related time entries
774 783 TimeEntry.update_all(["project_id = ?", project_id], {:issue_id => id})
775 784
776 785 # Delete issue relations
777 786 unless Setting.cross_project_issue_relations?
778 787 relations_from.clear
779 788 relations_to.clear
780 789 end
781 790
782 791 # Move subtasks
783 792 children.each do |child|
784 793 # Change project and keep project
785 794 child.send :project=, project, true
786 795 unless child.save
787 796 raise ActiveRecord::Rollback
788 797 end
789 798 end
790 799 end
791 800
792 801 def update_nested_set_attributes
793 802 if root_id.nil?
794 803 # issue was just created
795 804 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
796 805 set_default_left_and_right
797 806 Issue.update_all("root_id = #{root_id}, lft = #{lft}, rgt = #{rgt}", ["id = ?", id])
798 807 if @parent_issue
799 808 move_to_child_of(@parent_issue)
800 809 end
801 810 reload
802 811 elsif parent_issue_id != parent_id
803 812 former_parent_id = parent_id
804 813 # moving an existing issue
805 814 if @parent_issue && @parent_issue.root_id == root_id
806 815 # inside the same tree
807 816 move_to_child_of(@parent_issue)
808 817 else
809 818 # to another tree
810 819 unless root?
811 820 move_to_right_of(root)
812 821 reload
813 822 end
814 823 old_root_id = root_id
815 824 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
816 825 target_maxright = nested_set_scope.maximum(right_column_name) || 0
817 826 offset = target_maxright + 1 - lft
818 827 Issue.update_all("root_id = #{root_id}, lft = lft + #{offset}, rgt = rgt + #{offset}",
819 828 ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
820 829 self[left_column_name] = lft + offset
821 830 self[right_column_name] = rgt + offset
822 831 if @parent_issue
823 832 move_to_child_of(@parent_issue)
824 833 end
825 834 end
826 835 reload
827 836 # delete invalid relations of all descendants
828 837 self_and_descendants.each do |issue|
829 838 issue.relations.each do |relation|
830 839 relation.destroy unless relation.valid?
831 840 end
832 841 end
833 842 # update former parent
834 843 recalculate_attributes_for(former_parent_id) if former_parent_id
835 844 end
836 845 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
837 846 end
838 847
839 848 def update_parent_attributes
840 849 recalculate_attributes_for(parent_id) if parent_id
841 850 end
842 851
843 852 def recalculate_attributes_for(issue_id)
844 853 if issue_id && p = Issue.find_by_id(issue_id)
845 854 # priority = highest priority of children
846 855 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :joins => :priority)
847 856 p.priority = IssuePriority.find_by_position(priority_position)
848 857 end
849 858
850 859 # start/due dates = lowest/highest dates of children
851 860 p.start_date = p.children.minimum(:start_date)
852 861 p.due_date = p.children.maximum(:due_date)
853 862 if p.start_date && p.due_date && p.due_date < p.start_date
854 863 p.start_date, p.due_date = p.due_date, p.start_date
855 864 end
856 865
857 866 # done ratio = weighted average ratio of leaves
858 867 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
859 868 leaves_count = p.leaves.count
860 869 if leaves_count > 0
861 870 average = p.leaves.average(:estimated_hours).to_f
862 871 if average == 0
863 872 average = 1
864 873 end
865 874 done = p.leaves.sum("COALESCE(estimated_hours, #{average}) * (CASE WHEN is_closed = #{connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)", :joins => :status).to_f
866 875 progress = done / (average * leaves_count)
867 876 p.done_ratio = progress.round
868 877 end
869 878 end
870 879
871 880 # estimate = sum of leaves estimates
872 881 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
873 882 p.estimated_hours = nil if p.estimated_hours == 0.0
874 883
875 884 # ancestors will be recursively updated
876 885 p.save(false)
877 886 end
878 887 end
879 888
880 889 # Update issues so their versions are not pointing to a
881 890 # fixed_version that is not shared with the issue's project
882 891 def self.update_versions(conditions=nil)
883 892 # Only need to update issues with a fixed_version from
884 893 # a different project and that is not systemwide shared
885 894 Issue.scoped(:conditions => conditions).all(
886 895 :conditions => "#{Issue.table_name}.fixed_version_id IS NOT NULL" +
887 896 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
888 897 " AND #{Version.table_name}.sharing <> 'system'",
889 898 :include => [:project, :fixed_version]
890 899 ).each do |issue|
891 900 next if issue.project.nil? || issue.fixed_version.nil?
892 901 unless issue.project.shared_versions.include?(issue.fixed_version)
893 902 issue.init_journal(User.current)
894 903 issue.fixed_version = nil
895 904 issue.save
896 905 end
897 906 end
898 907 end
899 908
900 909 # Callback on attachment deletion
901 910 def attachment_added(obj)
902 911 if @current_journal && !obj.new_record?
903 912 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :value => obj.filename)
904 913 end
905 914 end
906 915
907 916 # Callback on attachment deletion
908 917 def attachment_removed(obj)
909 918 journal = init_journal(User.current)
910 919 journal.details << JournalDetail.new(:property => 'attachment',
911 920 :prop_key => obj.id,
912 921 :old_value => obj.filename)
913 922 journal.save
914 923 end
915 924
916 925 # Default assignment based on category
917 926 def default_assign
918 927 if assigned_to.nil? && category && category.assigned_to
919 928 self.assigned_to = category.assigned_to
920 929 end
921 930 end
922 931
923 932 # Updates start/due dates of following issues
924 933 def reschedule_following_issues
925 934 if start_date_changed? || due_date_changed?
926 935 relations_from.each do |relation|
927 936 relation.set_issue_to_dates
928 937 end
929 938 end
930 939 end
931 940
932 941 # Closes duplicates if the issue is being closed
933 942 def close_duplicates
934 943 if closing?
935 944 duplicates.each do |duplicate|
936 945 # Reload is need in case the duplicate was updated by a previous duplicate
937 946 duplicate.reload
938 947 # Don't re-close it if it's already closed
939 948 next if duplicate.closed?
940 949 # Same user and notes
941 950 if @current_journal
942 951 duplicate.init_journal(@current_journal.user, @current_journal.notes)
943 952 end
944 953 duplicate.update_attribute :status, self.status
945 954 end
946 955 end
947 956 end
948 957
949 958 # Saves the changes in a Journal
950 959 # Called after_save
951 960 def create_journal
952 961 if @current_journal
953 962 # attributes changes
954 963 if @attributes_before_change
955 964 (Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on)).each {|c|
956 965 before = @attributes_before_change[c]
957 966 after = send(c)
958 967 next if before == after || (before.blank? && after.blank?)
959 968 @current_journal.details << JournalDetail.new(:property => 'attr',
960 969 :prop_key => c,
961 970 :old_value => before,
962 971 :value => after)
963 972 }
964 973 end
965 974 if @custom_values_before_change
966 975 # custom fields changes
967 976 custom_values.each {|c|
968 977 before = @custom_values_before_change[c.custom_field_id]
969 978 after = c.value
970 979 next if before == after || (before.blank? && after.blank?)
971 980 @current_journal.details << JournalDetail.new(:property => 'cf',
972 981 :prop_key => c.custom_field_id,
973 982 :old_value => before,
974 983 :value => after)
975 984 }
976 985 end
977 986 @current_journal.save
978 987 # reset current journal
979 988 init_journal @current_journal.user, @current_journal.notes
980 989 end
981 990 end
982 991
983 992 # Query generator for selecting groups of issue counts for a project
984 993 # based on specific criteria
985 994 #
986 995 # Options
987 996 # * project - Project to search in.
988 997 # * field - String. Issue field to key off of in the grouping.
989 998 # * joins - String. The table name to join against.
990 999 def self.count_and_group_by(options)
991 1000 project = options.delete(:project)
992 1001 select_field = options.delete(:field)
993 1002 joins = options.delete(:joins)
994 1003
995 1004 where = "#{Issue.table_name}.#{select_field}=j.id"
996 1005
997 1006 ActiveRecord::Base.connection.select_all("select s.id as status_id,
998 1007 s.is_closed as closed,
999 1008 j.id as #{select_field},
1000 1009 count(#{Issue.table_name}.id) as total
1001 1010 from
1002 1011 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s, #{joins} j
1003 1012 where
1004 1013 #{Issue.table_name}.status_id=s.id
1005 1014 and #{where}
1006 1015 and #{Issue.table_name}.project_id=#{Project.table_name}.id
1007 1016 and #{visible_condition(User.current, :project => project)}
1008 1017 group by s.id, s.is_closed, j.id")
1009 1018 end
1010 1019 end
@@ -1,395 +1,399
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 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, :members, :member_roles, :roles,
22 22 :trackers, :projects_trackers,
23 23 :versions,
24 24 :issue_statuses, :issue_categories, :issue_relations, :workflows,
25 25 :enumerations,
26 26 :issues,
27 27 :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values,
28 28 :time_entries
29 29
30 30 self.use_transactional_fixtures = false
31 31
32 32 def test_create_root_issue
33 33 issue1 = create_issue!
34 34 issue2 = create_issue!
35 35 issue1.reload
36 36 issue2.reload
37 37
38 38 assert_equal [issue1.id, nil, 1, 2], [issue1.root_id, issue1.parent_id, issue1.lft, issue1.rgt]
39 39 assert_equal [issue2.id, nil, 1, 2], [issue2.root_id, issue2.parent_id, issue2.lft, issue2.rgt]
40 40 end
41 41
42 42 def test_create_child_issue
43 43 parent = create_issue!
44 44 child = create_issue!(:parent_issue_id => parent.id)
45 45 parent.reload
46 46 child.reload
47 47
48 48 assert_equal [parent.id, nil, 1, 4], [parent.root_id, parent.parent_id, parent.lft, parent.rgt]
49 49 assert_equal [parent.id, parent.id, 2, 3], [child.root_id, child.parent_id, child.lft, child.rgt]
50 50 end
51 51
52 52 def test_creating_a_child_in_different_project_should_not_validate
53 53 issue = create_issue!
54 54 child = Issue.new(:project_id => 2, :tracker_id => 1, :author_id => 1,
55 55 :subject => 'child', :parent_issue_id => issue.id)
56 56 assert !child.save
57 57 assert_not_nil child.errors[:parent_issue_id]
58 58 end
59 59
60 60 def test_move_a_root_to_child
61 61 parent1 = create_issue!
62 62 parent2 = create_issue!
63 63 child = create_issue!(:parent_issue_id => parent1.id)
64 64
65 65 parent2.parent_issue_id = parent1.id
66 66 parent2.save!
67 67 child.reload
68 68 parent1.reload
69 69 parent2.reload
70 70
71 71 assert_equal [parent1.id, 1, 6], [parent1.root_id, parent1.lft, parent1.rgt]
72 72 assert_equal [parent1.id, 4, 5], [parent2.root_id, parent2.lft, parent2.rgt]
73 73 assert_equal [parent1.id, 2, 3], [child.root_id, child.lft, child.rgt]
74 74 end
75 75
76 76 def test_move_a_child_to_root
77 77 parent1 = create_issue!
78 78 parent2 = create_issue!
79 79 child = create_issue!(:parent_issue_id => parent1.id)
80 80
81 81 child.parent_issue_id = nil
82 82 child.save!
83 83 child.reload
84 84 parent1.reload
85 85 parent2.reload
86 86
87 87 assert_equal [parent1.id, 1, 2], [parent1.root_id, parent1.lft, parent1.rgt]
88 88 assert_equal [parent2.id, 1, 2], [parent2.root_id, parent2.lft, parent2.rgt]
89 89 assert_equal [child.id, 1, 2], [child.root_id, child.lft, child.rgt]
90 90 end
91 91
92 92 def test_move_a_child_to_another_issue
93 93 parent1 = create_issue!
94 94 parent2 = create_issue!
95 95 child = create_issue!(:parent_issue_id => parent1.id)
96 96
97 97 child.parent_issue_id = parent2.id
98 98 child.save!
99 99 child.reload
100 100 parent1.reload
101 101 parent2.reload
102 102
103 103 assert_equal [parent1.id, 1, 2], [parent1.root_id, parent1.lft, parent1.rgt]
104 104 assert_equal [parent2.id, 1, 4], [parent2.root_id, parent2.lft, parent2.rgt]
105 105 assert_equal [parent2.id, 2, 3], [child.root_id, child.lft, child.rgt]
106 106 end
107 107
108 108 def test_move_a_child_with_descendants_to_another_issue
109 109 parent1 = create_issue!
110 110 parent2 = create_issue!
111 111 child = create_issue!(:parent_issue_id => parent1.id)
112 112 grandchild = create_issue!(:parent_issue_id => child.id)
113 113
114 114 parent1.reload
115 115 parent2.reload
116 116 child.reload
117 117 grandchild.reload
118 118
119 119 assert_equal [parent1.id, 1, 6], [parent1.root_id, parent1.lft, parent1.rgt]
120 120 assert_equal [parent2.id, 1, 2], [parent2.root_id, parent2.lft, parent2.rgt]
121 121 assert_equal [parent1.id, 2, 5], [child.root_id, child.lft, child.rgt]
122 122 assert_equal [parent1.id, 3, 4], [grandchild.root_id, grandchild.lft, grandchild.rgt]
123 123
124 124 child.reload.parent_issue_id = parent2.id
125 125 child.save!
126 126 child.reload
127 127 grandchild.reload
128 128 parent1.reload
129 129 parent2.reload
130 130
131 131 assert_equal [parent1.id, 1, 2], [parent1.root_id, parent1.lft, parent1.rgt]
132 132 assert_equal [parent2.id, 1, 6], [parent2.root_id, parent2.lft, parent2.rgt]
133 133 assert_equal [parent2.id, 2, 5], [child.root_id, child.lft, child.rgt]
134 134 assert_equal [parent2.id, 3, 4], [grandchild.root_id, grandchild.lft, grandchild.rgt]
135 135 end
136 136
137 137 def test_move_a_child_with_descendants_to_another_project
138 138 parent1 = create_issue!
139 139 child = create_issue!(:parent_issue_id => parent1.id)
140 140 grandchild = create_issue!(:parent_issue_id => child.id)
141 141
142 assert child.reload.move_to_project(Project.find(2))
142 child.reload
143 child.project = Project.find(2)
144 assert child.save
143 145 child.reload
144 146 grandchild.reload
145 147 parent1.reload
146 148
147 149 assert_equal [1, parent1.id, 1, 2], [parent1.project_id, parent1.root_id, parent1.lft, parent1.rgt]
148 150 assert_equal [2, child.id, 1, 4], [child.project_id, child.root_id, child.lft, child.rgt]
149 151 assert_equal [2, child.id, 2, 3], [grandchild.project_id, grandchild.root_id, grandchild.lft, grandchild.rgt]
150 152 end
151 153
152 154 def test_invalid_move_to_another_project
153 155 parent1 = create_issue!
154 156 child = create_issue!(:parent_issue_id => parent1.id)
155 157 grandchild = create_issue!(:parent_issue_id => child.id, :tracker_id => 2)
156 158 Project.find(2).tracker_ids = [1]
157 159
158 160 parent1.reload
159 161 assert_equal [1, parent1.id, 1, 6], [parent1.project_id, parent1.root_id, parent1.lft, parent1.rgt]
160 162
161 163 # child can not be moved to Project 2 because its child is on a disabled tracker
162 assert_equal false, Issue.find(child.id).move_to_project(Project.find(2))
164 child = Issue.find(child.id)
165 child.project = Project.find(2)
166 assert !child.save
163 167 child.reload
164 168 grandchild.reload
165 169 parent1.reload
166 170
167 171 # no change
168 172 assert_equal [1, parent1.id, 1, 6], [parent1.project_id, parent1.root_id, parent1.lft, parent1.rgt]
169 173 assert_equal [1, parent1.id, 2, 5], [child.project_id, child.root_id, child.lft, child.rgt]
170 174 assert_equal [1, parent1.id, 3, 4], [grandchild.project_id, grandchild.root_id, grandchild.lft, grandchild.rgt]
171 175 end
172 176
173 177 def test_moving_an_issue_to_a_descendant_should_not_validate
174 178 parent1 = create_issue!
175 179 parent2 = create_issue!
176 180 child = create_issue!(:parent_issue_id => parent1.id)
177 181 grandchild = create_issue!(:parent_issue_id => child.id)
178 182
179 183 child.reload
180 184 child.parent_issue_id = grandchild.id
181 185 assert !child.save
182 186 assert_not_nil child.errors[:parent_issue_id]
183 187 end
184 188
185 189 def test_moving_an_issue_should_keep_valid_relations_only
186 190 issue1 = create_issue!
187 191 issue2 = create_issue!
188 192 issue3 = create_issue!(:parent_issue_id => issue2.id)
189 193 issue4 = create_issue!
190 194 r1 = IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_PRECEDES)
191 195 r2 = IssueRelation.create!(:issue_from => issue1, :issue_to => issue3, :relation_type => IssueRelation::TYPE_PRECEDES)
192 196 r3 = IssueRelation.create!(:issue_from => issue2, :issue_to => issue4, :relation_type => IssueRelation::TYPE_PRECEDES)
193 197 issue2.reload
194 198 issue2.parent_issue_id = issue1.id
195 199 issue2.save!
196 200 assert !IssueRelation.exists?(r1.id)
197 201 assert !IssueRelation.exists?(r2.id)
198 202 assert IssueRelation.exists?(r3.id)
199 203 end
200 204
201 205 def test_destroy_should_destroy_children
202 206 issue1 = create_issue!
203 207 issue2 = create_issue!
204 208 issue3 = create_issue!(:parent_issue_id => issue2.id)
205 209 issue4 = create_issue!(:parent_issue_id => issue1.id)
206 210
207 211 issue3.init_journal(User.find(2))
208 212 issue3.subject = 'child with journal'
209 213 issue3.save!
210 214
211 215 assert_difference 'Issue.count', -2 do
212 216 assert_difference 'Journal.count', -1 do
213 217 assert_difference 'JournalDetail.count', -1 do
214 218 Issue.find(issue2.id).destroy
215 219 end
216 220 end
217 221 end
218 222
219 223 issue1.reload
220 224 issue4.reload
221 225 assert !Issue.exists?(issue2.id)
222 226 assert !Issue.exists?(issue3.id)
223 227 assert_equal [issue1.id, 1, 4], [issue1.root_id, issue1.lft, issue1.rgt]
224 228 assert_equal [issue1.id, 2, 3], [issue4.root_id, issue4.lft, issue4.rgt]
225 229 end
226 230
227 231 def test_destroy_child_should_update_parent
228 232 issue = create_issue!
229 233 child1 = create_issue!(:parent_issue_id => issue.id)
230 234 child2 = create_issue!(:parent_issue_id => issue.id)
231 235
232 236 issue.reload
233 237 assert_equal [issue.id, 1, 6], [issue.root_id, issue.lft, issue.rgt]
234 238
235 239 child2.reload.destroy
236 240
237 241 issue.reload
238 242 assert_equal [issue.id, 1, 4], [issue.root_id, issue.lft, issue.rgt]
239 243 end
240 244
241 245 def test_destroy_parent_issue_updated_during_children_destroy
242 246 parent = create_issue!
243 247 create_issue!(:start_date => Date.today, :parent_issue_id => parent.id)
244 248 create_issue!(:start_date => 2.days.from_now, :parent_issue_id => parent.id)
245 249
246 250 assert_difference 'Issue.count', -3 do
247 251 Issue.find(parent.id).destroy
248 252 end
249 253 end
250 254
251 255 def test_destroy_child_issue_with_children
252 256 root = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'root')
253 257 child = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'child', :parent_issue_id => root.id)
254 258 leaf = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'leaf', :parent_issue_id => child.id)
255 259 leaf.init_journal(User.find(2))
256 260 leaf.subject = 'leaf with journal'
257 261 leaf.save!
258 262
259 263 assert_difference 'Issue.count', -2 do
260 264 assert_difference 'Journal.count', -1 do
261 265 assert_difference 'JournalDetail.count', -1 do
262 266 Issue.find(child.id).destroy
263 267 end
264 268 end
265 269 end
266 270
267 271 root = Issue.find(root.id)
268 272 assert root.leaf?, "Root issue is not a leaf (lft: #{root.lft}, rgt: #{root.rgt})"
269 273 end
270 274
271 275 def test_destroy_issue_with_grand_child
272 276 parent = create_issue!
273 277 issue = create_issue!(:parent_issue_id => parent.id)
274 278 child = create_issue!(:parent_issue_id => issue.id)
275 279 grandchild1 = create_issue!(:parent_issue_id => child.id)
276 280 grandchild2 = create_issue!(:parent_issue_id => child.id)
277 281
278 282 assert_difference 'Issue.count', -4 do
279 283 Issue.find(issue.id).destroy
280 284 parent.reload
281 285 assert_equal [1, 2], [parent.lft, parent.rgt]
282 286 end
283 287 end
284 288
285 289 def test_parent_priority_should_be_the_highest_child_priority
286 290 parent = create_issue!(:priority => IssuePriority.find_by_name('Normal'))
287 291 # Create children
288 292 child1 = create_issue!(:priority => IssuePriority.find_by_name('High'), :parent_issue_id => parent.id)
289 293 assert_equal 'High', parent.reload.priority.name
290 294 child2 = create_issue!(:priority => IssuePriority.find_by_name('Immediate'), :parent_issue_id => child1.id)
291 295 assert_equal 'Immediate', child1.reload.priority.name
292 296 assert_equal 'Immediate', parent.reload.priority.name
293 297 child3 = create_issue!(:priority => IssuePriority.find_by_name('Low'), :parent_issue_id => parent.id)
294 298 assert_equal 'Immediate', parent.reload.priority.name
295 299 # Destroy a child
296 300 child1.destroy
297 301 assert_equal 'Low', parent.reload.priority.name
298 302 # Update a child
299 303 child3.reload.priority = IssuePriority.find_by_name('Normal')
300 304 child3.save!
301 305 assert_equal 'Normal', parent.reload.priority.name
302 306 end
303 307
304 308 def test_parent_dates_should_be_lowest_start_and_highest_due_dates
305 309 parent = create_issue!
306 310 create_issue!(:start_date => '2010-01-25', :due_date => '2010-02-15', :parent_issue_id => parent.id)
307 311 create_issue!( :due_date => '2010-02-13', :parent_issue_id => parent.id)
308 312 create_issue!(:start_date => '2010-02-01', :due_date => '2010-02-22', :parent_issue_id => parent.id)
309 313 parent.reload
310 314 assert_equal Date.parse('2010-01-25'), parent.start_date
311 315 assert_equal Date.parse('2010-02-22'), parent.due_date
312 316 end
313 317
314 318 def test_parent_done_ratio_should_be_average_done_ratio_of_leaves
315 319 parent = create_issue!
316 320 create_issue!(:done_ratio => 20, :parent_issue_id => parent.id)
317 321 assert_equal 20, parent.reload.done_ratio
318 322 create_issue!(:done_ratio => 70, :parent_issue_id => parent.id)
319 323 assert_equal 45, parent.reload.done_ratio
320 324
321 325 child = create_issue!(:done_ratio => 0, :parent_issue_id => parent.id)
322 326 assert_equal 30, parent.reload.done_ratio
323 327
324 328 create_issue!(:done_ratio => 30, :parent_issue_id => child.id)
325 329 assert_equal 30, child.reload.done_ratio
326 330 assert_equal 40, parent.reload.done_ratio
327 331 end
328 332
329 333 def test_parent_done_ratio_should_be_weighted_by_estimated_times_if_any
330 334 parent = create_issue!
331 335 create_issue!(:estimated_hours => 10, :done_ratio => 20, :parent_issue_id => parent.id)
332 336 assert_equal 20, parent.reload.done_ratio
333 337 create_issue!(:estimated_hours => 20, :done_ratio => 50, :parent_issue_id => parent.id)
334 338 assert_equal (50 * 20 + 20 * 10) / 30, parent.reload.done_ratio
335 339 end
336 340
337 341 def test_parent_estimate_should_be_sum_of_leaves
338 342 parent = create_issue!
339 343 create_issue!(:estimated_hours => nil, :parent_issue_id => parent.id)
340 344 assert_equal nil, parent.reload.estimated_hours
341 345 create_issue!(:estimated_hours => 5, :parent_issue_id => parent.id)
342 346 assert_equal 5, parent.reload.estimated_hours
343 347 create_issue!(:estimated_hours => 7, :parent_issue_id => parent.id)
344 348 assert_equal 12, parent.reload.estimated_hours
345 349 end
346 350
347 351 def test_move_parent_updates_old_parent_attributes
348 352 first_parent = create_issue!
349 353 second_parent = create_issue!
350 354 child = create_issue!(:estimated_hours => 5, :parent_issue_id => first_parent.id)
351 355 assert_equal 5, first_parent.reload.estimated_hours
352 356 child.update_attributes(:estimated_hours => 7, :parent_issue_id => second_parent.id)
353 357 assert_equal 7, second_parent.reload.estimated_hours
354 358 assert_nil first_parent.reload.estimated_hours
355 359 end
356 360
357 361 def test_reschuling_a_parent_should_reschedule_subtasks
358 362 parent = create_issue!
359 363 c1 = create_issue!(:start_date => '2010-05-12', :due_date => '2010-05-18', :parent_issue_id => parent.id)
360 364 c2 = create_issue!(:start_date => '2010-06-03', :due_date => '2010-06-10', :parent_issue_id => parent.id)
361 365 parent.reload
362 366 parent.reschedule_after(Date.parse('2010-06-02'))
363 367 c1.reload
364 368 assert_equal [Date.parse('2010-06-02'), Date.parse('2010-06-08')], [c1.start_date, c1.due_date]
365 369 c2.reload
366 370 assert_equal [Date.parse('2010-06-03'), Date.parse('2010-06-10')], [c2.start_date, c2.due_date] # no change
367 371 parent.reload
368 372 assert_equal [Date.parse('2010-06-02'), Date.parse('2010-06-10')], [parent.start_date, parent.due_date]
369 373 end
370 374
371 375 def test_project_copy_should_copy_issue_tree
372 376 p = Project.create!(:name => 'Tree copy', :identifier => 'tree-copy', :tracker_ids => [1, 2])
373 377 i1 = create_issue!(:project_id => p.id, :subject => 'i1')
374 378 i2 = create_issue!(:project_id => p.id, :subject => 'i2', :parent_issue_id => i1.id)
375 379 i3 = create_issue!(:project_id => p.id, :subject => 'i3', :parent_issue_id => i1.id)
376 380 i4 = create_issue!(:project_id => p.id, :subject => 'i4', :parent_issue_id => i2.id)
377 381 i5 = create_issue!(:project_id => p.id, :subject => 'i5')
378 382 c = Project.new(:name => 'Copy', :identifier => 'copy', :tracker_ids => [1, 2])
379 383 c.copy(p, :only => 'issues')
380 384 c.reload
381 385
382 386 assert_equal 5, c.issues.count
383 387 ic1, ic2, ic3, ic4, ic5 = c.issues.find(:all, :order => 'subject')
384 388 assert ic1.root?
385 389 assert_equal ic1, ic2.parent
386 390 assert_equal ic1, ic3.parent
387 391 assert_equal ic2, ic4.parent
388 392 assert ic5.root?
389 393 end
390 394
391 395 # Helper that creates an issue with default attributes
392 396 def create_issue!(attributes={})
393 397 Issue.create!({:project_id => 1, :tracker_id => 1, :author_id => 1, :subject => 'test'}.merge(attributes))
394 398 end
395 399 end
@@ -1,1184 +1,1190
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 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 IssueTest < ActiveSupport::TestCase
21 21 fixtures :projects, :users, :members, :member_roles, :roles,
22 22 :trackers, :projects_trackers,
23 23 :enabled_modules,
24 24 :versions,
25 25 :issue_statuses, :issue_categories, :issue_relations, :workflows,
26 26 :enumerations,
27 27 :issues,
28 28 :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values,
29 29 :time_entries
30 30
31 31 def test_create
32 32 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
33 33 :status_id => 1, :priority => IssuePriority.all.first,
34 34 :subject => 'test_create',
35 35 :description => 'IssueTest#test_create', :estimated_hours => '1:30')
36 36 assert issue.save
37 37 issue.reload
38 38 assert_equal 1.5, issue.estimated_hours
39 39 end
40 40
41 41 def test_create_minimal
42 42 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
43 43 :status_id => 1, :priority => IssuePriority.all.first,
44 44 :subject => 'test_create')
45 45 assert issue.save
46 46 assert issue.description.nil?
47 47 end
48 48
49 49 def test_create_with_required_custom_field
50 50 field = IssueCustomField.find_by_name('Database')
51 51 field.update_attribute(:is_required, true)
52 52
53 53 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
54 54 :status_id => 1, :subject => 'test_create',
55 55 :description => 'IssueTest#test_create_with_required_custom_field')
56 56 assert issue.available_custom_fields.include?(field)
57 57 # No value for the custom field
58 58 assert !issue.save
59 59 assert_equal I18n.translate('activerecord.errors.messages.invalid'),
60 60 issue.errors[:custom_values].to_s
61 61 # Blank value
62 62 issue.custom_field_values = { field.id => '' }
63 63 assert !issue.save
64 64 assert_equal I18n.translate('activerecord.errors.messages.invalid'),
65 65 issue.errors[:custom_values].to_s
66 66 # Invalid value
67 67 issue.custom_field_values = { field.id => 'SQLServer' }
68 68 assert !issue.save
69 69 assert_equal I18n.translate('activerecord.errors.messages.invalid'),
70 70 issue.errors[:custom_values].to_s
71 71 # Valid value
72 72 issue.custom_field_values = { field.id => 'PostgreSQL' }
73 73 assert issue.save
74 74 issue.reload
75 75 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
76 76 end
77 77
78 78 def test_create_with_group_assignment
79 79 with_settings :issue_group_assignment => '1' do
80 80 assert Issue.new(:project_id => 2, :tracker_id => 1, :author_id => 1,
81 81 :subject => 'Group assignment',
82 82 :assigned_to_id => 11).save
83 83 issue = Issue.first(:order => 'id DESC')
84 84 assert_kind_of Group, issue.assigned_to
85 85 assert_equal Group.find(11), issue.assigned_to
86 86 end
87 87 end
88 88
89 89 def assert_visibility_match(user, issues)
90 90 assert_equal issues.collect(&:id).sort, Issue.all.select {|issue| issue.visible?(user)}.collect(&:id).sort
91 91 end
92 92
93 93 def test_visible_scope_for_anonymous
94 94 # Anonymous user should see issues of public projects only
95 95 issues = Issue.visible(User.anonymous).all
96 96 assert issues.any?
97 97 assert_nil issues.detect {|issue| !issue.project.is_public?}
98 98 assert_nil issues.detect {|issue| issue.is_private?}
99 99 assert_visibility_match User.anonymous, issues
100 100 end
101 101
102 102 def test_visible_scope_for_anonymous_with_own_issues_visibility
103 103 Role.anonymous.update_attribute :issues_visibility, 'own'
104 104 Issue.create!(:project_id => 1, :tracker_id => 1,
105 105 :author_id => User.anonymous.id,
106 106 :subject => 'Issue by anonymous')
107 107
108 108 issues = Issue.visible(User.anonymous).all
109 109 assert issues.any?
110 110 assert_nil issues.detect {|issue| issue.author != User.anonymous}
111 111 assert_visibility_match User.anonymous, issues
112 112 end
113 113
114 114 def test_visible_scope_for_anonymous_without_view_issues_permissions
115 115 # Anonymous user should not see issues without permission
116 116 Role.anonymous.remove_permission!(:view_issues)
117 117 issues = Issue.visible(User.anonymous).all
118 118 assert issues.empty?
119 119 assert_visibility_match User.anonymous, issues
120 120 end
121 121
122 122 def test_visible_scope_for_non_member
123 123 user = User.find(9)
124 124 assert user.projects.empty?
125 125 # Non member user should see issues of public projects only
126 126 issues = Issue.visible(user).all
127 127 assert issues.any?
128 128 assert_nil issues.detect {|issue| !issue.project.is_public?}
129 129 assert_nil issues.detect {|issue| issue.is_private?}
130 130 assert_visibility_match user, issues
131 131 end
132 132
133 133 def test_visible_scope_for_non_member_with_own_issues_visibility
134 134 Role.non_member.update_attribute :issues_visibility, 'own'
135 135 Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 9, :subject => 'Issue by non member')
136 136 user = User.find(9)
137 137
138 138 issues = Issue.visible(user).all
139 139 assert issues.any?
140 140 assert_nil issues.detect {|issue| issue.author != user}
141 141 assert_visibility_match user, issues
142 142 end
143 143
144 144 def test_visible_scope_for_non_member_without_view_issues_permissions
145 145 # Non member user should not see issues without permission
146 146 Role.non_member.remove_permission!(:view_issues)
147 147 user = User.find(9)
148 148 assert user.projects.empty?
149 149 issues = Issue.visible(user).all
150 150 assert issues.empty?
151 151 assert_visibility_match user, issues
152 152 end
153 153
154 154 def test_visible_scope_for_member
155 155 user = User.find(9)
156 156 # User should see issues of projects for which he has view_issues permissions only
157 157 Role.non_member.remove_permission!(:view_issues)
158 158 Member.create!(:principal => user, :project_id => 3, :role_ids => [2])
159 159 issues = Issue.visible(user).all
160 160 assert issues.any?
161 161 assert_nil issues.detect {|issue| issue.project_id != 3}
162 162 assert_nil issues.detect {|issue| issue.is_private?}
163 163 assert_visibility_match user, issues
164 164 end
165 165
166 166 def test_visible_scope_for_member_with_groups_should_return_assigned_issues
167 167 user = User.find(8)
168 168 assert user.groups.any?
169 169 Member.create!(:principal => user.groups.first, :project_id => 1, :role_ids => [2])
170 170 Role.non_member.remove_permission!(:view_issues)
171 171
172 172 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3,
173 173 :status_id => 1, :priority => IssuePriority.all.first,
174 174 :subject => 'Assignment test',
175 175 :assigned_to => user.groups.first,
176 176 :is_private => true)
177 177
178 178 Role.find(2).update_attribute :issues_visibility, 'default'
179 179 issues = Issue.visible(User.find(8)).all
180 180 assert issues.any?
181 181 assert issues.include?(issue)
182 182
183 183 Role.find(2).update_attribute :issues_visibility, 'own'
184 184 issues = Issue.visible(User.find(8)).all
185 185 assert issues.any?
186 186 assert issues.include?(issue)
187 187 end
188 188
189 189 def test_visible_scope_for_admin
190 190 user = User.find(1)
191 191 user.members.each(&:destroy)
192 192 assert user.projects.empty?
193 193 issues = Issue.visible(user).all
194 194 assert issues.any?
195 195 # Admin should see issues on private projects that he does not belong to
196 196 assert issues.detect {|issue| !issue.project.is_public?}
197 197 # Admin should see private issues of other users
198 198 assert issues.detect {|issue| issue.is_private? && issue.author != user}
199 199 assert_visibility_match user, issues
200 200 end
201 201
202 202 def test_visible_scope_with_project
203 203 project = Project.find(1)
204 204 issues = Issue.visible(User.find(2), :project => project).all
205 205 projects = issues.collect(&:project).uniq
206 206 assert_equal 1, projects.size
207 207 assert_equal project, projects.first
208 208 end
209 209
210 210 def test_visible_scope_with_project_and_subprojects
211 211 project = Project.find(1)
212 212 issues = Issue.visible(User.find(2), :project => project, :with_subprojects => true).all
213 213 projects = issues.collect(&:project).uniq
214 214 assert projects.size > 1
215 215 assert_equal [], projects.select {|p| !p.is_or_is_descendant_of?(project)}
216 216 end
217 217
218 218 def test_visible_and_nested_set_scopes
219 219 assert_equal 0, Issue.find(1).descendants.visible.all.size
220 220 end
221 221
222 222 def test_open_scope
223 223 issues = Issue.open.all
224 224 assert_nil issues.detect(&:closed?)
225 225 end
226 226
227 227 def test_open_scope_with_arg
228 228 issues = Issue.open(false).all
229 229 assert_equal issues, issues.select(&:closed?)
230 230 end
231 231
232 232 def test_errors_full_messages_should_include_custom_fields_errors
233 233 field = IssueCustomField.find_by_name('Database')
234 234
235 235 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
236 236 :status_id => 1, :subject => 'test_create',
237 237 :description => 'IssueTest#test_create_with_required_custom_field')
238 238 assert issue.available_custom_fields.include?(field)
239 239 # Invalid value
240 240 issue.custom_field_values = { field.id => 'SQLServer' }
241 241
242 242 assert !issue.valid?
243 243 assert_equal 1, issue.errors.full_messages.size
244 244 assert_equal "Database #{I18n.translate('activerecord.errors.messages.inclusion')}",
245 245 issue.errors.full_messages.first
246 246 end
247 247
248 248 def test_update_issue_with_required_custom_field
249 249 field = IssueCustomField.find_by_name('Database')
250 250 field.update_attribute(:is_required, true)
251 251
252 252 issue = Issue.find(1)
253 253 assert_nil issue.custom_value_for(field)
254 254 assert issue.available_custom_fields.include?(field)
255 255 # No change to custom values, issue can be saved
256 256 assert issue.save
257 257 # Blank value
258 258 issue.custom_field_values = { field.id => '' }
259 259 assert !issue.save
260 260 # Valid value
261 261 issue.custom_field_values = { field.id => 'PostgreSQL' }
262 262 assert issue.save
263 263 issue.reload
264 264 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
265 265 end
266 266
267 267 def test_should_not_update_attributes_if_custom_fields_validation_fails
268 268 issue = Issue.find(1)
269 269 field = IssueCustomField.find_by_name('Database')
270 270 assert issue.available_custom_fields.include?(field)
271 271
272 272 issue.custom_field_values = { field.id => 'Invalid' }
273 273 issue.subject = 'Should be not be saved'
274 274 assert !issue.save
275 275
276 276 issue.reload
277 277 assert_equal "Can't print recipes", issue.subject
278 278 end
279 279
280 280 def test_should_not_recreate_custom_values_objects_on_update
281 281 field = IssueCustomField.find_by_name('Database')
282 282
283 283 issue = Issue.find(1)
284 284 issue.custom_field_values = { field.id => 'PostgreSQL' }
285 285 assert issue.save
286 286 custom_value = issue.custom_value_for(field)
287 287 issue.reload
288 288 issue.custom_field_values = { field.id => 'MySQL' }
289 289 assert issue.save
290 290 issue.reload
291 291 assert_equal custom_value.id, issue.custom_value_for(field).id
292 292 end
293 293
294 294 def test_should_not_update_custom_fields_on_changing_tracker_with_different_custom_fields
295 295 issue = Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => 'Test', :custom_field_values => {'2' => 'Test'})
296 296 assert !Tracker.find(2).custom_field_ids.include?(2)
297 297
298 298 issue = Issue.find(issue.id)
299 299 issue.attributes = {:tracker_id => 2, :custom_field_values => {'1' => ''}}
300 300
301 301 issue = Issue.find(issue.id)
302 302 custom_value = issue.custom_value_for(2)
303 303 assert_not_nil custom_value
304 304 assert_equal 'Test', custom_value.value
305 305 end
306 306
307 307 def test_assigning_tracker_id_should_reload_custom_fields_values
308 308 issue = Issue.new(:project => Project.find(1))
309 309 assert issue.custom_field_values.empty?
310 310 issue.tracker_id = 1
311 311 assert issue.custom_field_values.any?
312 312 end
313 313
314 314 def test_assigning_attributes_should_assign_project_and_tracker_first
315 315 seq = sequence('seq')
316 316 issue = Issue.new
317 317 issue.expects(:project_id=).in_sequence(seq)
318 318 issue.expects(:tracker_id=).in_sequence(seq)
319 319 issue.expects(:subject=).in_sequence(seq)
320 320 issue.attributes = {:tracker_id => 2, :project_id => 1, :subject => 'Test'}
321 321 end
322 322
323 323 def test_assigning_tracker_and_custom_fields_should_assign_custom_fields
324 324 attributes = ActiveSupport::OrderedHash.new
325 325 attributes['custom_field_values'] = { '1' => 'MySQL' }
326 326 attributes['tracker_id'] = '1'
327 327 issue = Issue.new(:project => Project.find(1))
328 328 issue.attributes = attributes
329 329 assert_not_nil issue.custom_value_for(1)
330 330 assert_equal 'MySQL', issue.custom_value_for(1).value
331 331 end
332 332
333 333 def test_should_update_issue_with_disabled_tracker
334 334 p = Project.find(1)
335 335 issue = Issue.find(1)
336 336
337 337 p.trackers.delete(issue.tracker)
338 338 assert !p.trackers.include?(issue.tracker)
339 339
340 340 issue.reload
341 341 issue.subject = 'New subject'
342 342 assert issue.save
343 343 end
344 344
345 345 def test_should_not_set_a_disabled_tracker
346 346 p = Project.find(1)
347 347 p.trackers.delete(Tracker.find(2))
348 348
349 349 issue = Issue.find(1)
350 350 issue.tracker_id = 2
351 351 issue.subject = 'New subject'
352 352 assert !issue.save
353 353 assert_not_nil issue.errors[:tracker_id]
354 354 end
355 355
356 356 def test_category_based_assignment
357 357 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3,
358 358 :status_id => 1, :priority => IssuePriority.all.first,
359 359 :subject => 'Assignment test',
360 360 :description => 'Assignment test', :category_id => 1)
361 361 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
362 362 end
363 363
364 364 def test_new_statuses_allowed_to
365 365 Workflow.delete_all
366 366
367 367 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 2, :author => false, :assignee => false)
368 368 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 3, :author => true, :assignee => false)
369 369 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 4, :author => false, :assignee => true)
370 370 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 5, :author => true, :assignee => true)
371 371 status = IssueStatus.find(1)
372 372 role = Role.find(1)
373 373 tracker = Tracker.find(1)
374 374 user = User.find(2)
375 375
376 376 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1)
377 377 assert_equal [1, 2], issue.new_statuses_allowed_to(user).map(&:id)
378 378
379 379 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1, :author => user)
380 380 assert_equal [1, 2, 3, 5], issue.new_statuses_allowed_to(user).map(&:id)
381 381
382 382 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1, :assigned_to => user)
383 383 assert_equal [1, 2, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
384 384
385 385 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1, :author => user, :assigned_to => user)
386 386 assert_equal [1, 2, 3, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
387 387 end
388 388
389 389 def test_copy
390 390 issue = Issue.new.copy_from(1)
391 391 assert issue.save
392 392 issue.reload
393 393 orig = Issue.find(1)
394 394 assert_equal orig.subject, issue.subject
395 395 assert_equal orig.tracker, issue.tracker
396 396 assert_equal "125", issue.custom_value_for(2).value
397 397 end
398 398
399 399 def test_copy_should_copy_status
400 400 orig = Issue.find(8)
401 401 assert orig.status != IssueStatus.default
402 402
403 403 issue = Issue.new.copy_from(orig)
404 404 assert issue.save
405 405 issue.reload
406 406 assert_equal orig.status, issue.status
407 407 end
408 408
409 409 def test_should_not_call_after_project_change_on_creation
410 410 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1, :subject => 'Test', :author_id => 1)
411 411 issue.expects(:after_project_change).never
412 412 issue.save!
413 413 end
414 414
415 415 def test_should_not_call_after_project_change_on_update
416 416 issue = Issue.find(1)
417 417 issue.project = Project.find(1)
418 418 issue.subject = 'No project change'
419 419 issue.expects(:after_project_change).never
420 420 issue.save!
421 421 end
422 422
423 423 def test_should_call_after_project_change_on_project_change
424 424 issue = Issue.find(1)
425 425 issue.project = Project.find(2)
426 426 issue.expects(:after_project_change).once
427 427 issue.save!
428 428 end
429 429
430 430 def test_should_close_duplicates
431 431 # Create 3 issues
432 432 project = Project.find(1)
433 433 issue1 = Issue.generate_for_project!(project)
434 434 issue2 = Issue.generate_for_project!(project)
435 435 issue3 = Issue.generate_for_project!(project)
436 436
437 437 # 2 is a dupe of 1
438 438 IssueRelation.create!(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
439 439 # And 3 is a dupe of 2
440 440 IssueRelation.create!(:issue_from => issue3, :issue_to => issue2, :relation_type => IssueRelation::TYPE_DUPLICATES)
441 441 # And 3 is a dupe of 1 (circular duplicates)
442 442 IssueRelation.create!(:issue_from => issue3, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
443 443
444 444 assert issue1.reload.duplicates.include?(issue2)
445 445
446 446 # Closing issue 1
447 447 issue1.init_journal(User.find(:first), "Closing issue1")
448 448 issue1.status = IssueStatus.find :first, :conditions => {:is_closed => true}
449 449 assert issue1.save
450 450 # 2 and 3 should be also closed
451 451 assert issue2.reload.closed?
452 452 assert issue3.reload.closed?
453 453 end
454 454
455 455 def test_should_not_close_duplicated_issue
456 456 project = Project.find(1)
457 457 issue1 = Issue.generate_for_project!(project)
458 458 issue2 = Issue.generate_for_project!(project)
459 459
460 460 # 2 is a dupe of 1
461 461 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
462 462 # 2 is a dup of 1 but 1 is not a duplicate of 2
463 463 assert !issue2.reload.duplicates.include?(issue1)
464 464
465 465 # Closing issue 2
466 466 issue2.init_journal(User.find(:first), "Closing issue2")
467 467 issue2.status = IssueStatus.find :first, :conditions => {:is_closed => true}
468 468 assert issue2.save
469 469 # 1 should not be also closed
470 470 assert !issue1.reload.closed?
471 471 end
472 472
473 473 def test_assignable_versions
474 474 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
475 475 assert_equal ['open'], issue.assignable_versions.collect(&:status).uniq
476 476 end
477 477
478 478 def test_should_not_be_able_to_assign_a_new_issue_to_a_closed_version
479 479 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
480 480 assert !issue.save
481 481 assert_not_nil issue.errors[:fixed_version_id]
482 482 end
483 483
484 484 def test_should_not_be_able_to_assign_a_new_issue_to_a_locked_version
485 485 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 2, :subject => 'New issue')
486 486 assert !issue.save
487 487 assert_not_nil issue.errors[:fixed_version_id]
488 488 end
489 489
490 490 def test_should_be_able_to_assign_a_new_issue_to_an_open_version
491 491 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 3, :subject => 'New issue')
492 492 assert issue.save
493 493 end
494 494
495 495 def test_should_be_able_to_update_an_issue_assigned_to_a_closed_version
496 496 issue = Issue.find(11)
497 497 assert_equal 'closed', issue.fixed_version.status
498 498 issue.subject = 'Subject changed'
499 499 assert issue.save
500 500 end
501 501
502 502 def test_should_not_be_able_to_reopen_an_issue_assigned_to_a_closed_version
503 503 issue = Issue.find(11)
504 504 issue.status_id = 1
505 505 assert !issue.save
506 506 assert_not_nil issue.errors[:base]
507 507 end
508 508
509 509 def test_should_be_able_to_reopen_and_reassign_an_issue_assigned_to_a_closed_version
510 510 issue = Issue.find(11)
511 511 issue.status_id = 1
512 512 issue.fixed_version_id = 3
513 513 assert issue.save
514 514 end
515 515
516 516 def test_should_be_able_to_reopen_an_issue_assigned_to_a_locked_version
517 517 issue = Issue.find(12)
518 518 assert_equal 'locked', issue.fixed_version.status
519 519 issue.status_id = 1
520 520 assert issue.save
521 521 end
522 522
523 523 def test_move_to_another_project_with_same_category
524 524 issue = Issue.find(1)
525 assert issue.move_to_project(Project.find(2))
525 issue.project = Project.find(2)
526 assert issue.save
526 527 issue.reload
527 528 assert_equal 2, issue.project_id
528 529 # Category changes
529 530 assert_equal 4, issue.category_id
530 531 # Make sure time entries were move to the target project
531 532 assert_equal 2, issue.time_entries.first.project_id
532 533 end
533 534
534 535 def test_move_to_another_project_without_same_category
535 536 issue = Issue.find(2)
536 assert issue.move_to_project(Project.find(2))
537 issue.project = Project.find(2)
538 assert issue.save
537 539 issue.reload
538 540 assert_equal 2, issue.project_id
539 541 # Category cleared
540 542 assert_nil issue.category_id
541 543 end
542 544
543 545 def test_move_to_another_project_should_clear_fixed_version_when_not_shared
544 546 issue = Issue.find(1)
545 547 issue.update_attribute(:fixed_version_id, 1)
546 assert issue.move_to_project(Project.find(2))
548 issue.project = Project.find(2)
549 assert issue.save
547 550 issue.reload
548 551 assert_equal 2, issue.project_id
549 552 # Cleared fixed_version
550 553 assert_equal nil, issue.fixed_version
551 554 end
552 555
553 556 def test_move_to_another_project_should_keep_fixed_version_when_shared_with_the_target_project
554 557 issue = Issue.find(1)
555 558 issue.update_attribute(:fixed_version_id, 4)
556 assert issue.move_to_project(Project.find(5))
559 issue.project = Project.find(5)
560 assert issue.save
557 561 issue.reload
558 562 assert_equal 5, issue.project_id
559 563 # Keep fixed_version
560 564 assert_equal 4, issue.fixed_version_id
561 565 end
562 566
563 567 def test_move_to_another_project_should_clear_fixed_version_when_not_shared_with_the_target_project
564 568 issue = Issue.find(1)
565 569 issue.update_attribute(:fixed_version_id, 1)
566 assert issue.move_to_project(Project.find(5))
570 issue.project = Project.find(5)
571 assert issue.save
567 572 issue.reload
568 573 assert_equal 5, issue.project_id
569 574 # Cleared fixed_version
570 575 assert_equal nil, issue.fixed_version
571 576 end
572 577
573 578 def test_move_to_another_project_should_keep_fixed_version_when_shared_systemwide
574 579 issue = Issue.find(1)
575 580 issue.update_attribute(:fixed_version_id, 7)
576 assert issue.move_to_project(Project.find(2))
581 issue.project = Project.find(2)
582 assert issue.save
577 583 issue.reload
578 584 assert_equal 2, issue.project_id
579 585 # Keep fixed_version
580 586 assert_equal 7, issue.fixed_version_id
581 587 end
582 588
583 589 def test_move_to_another_project_with_disabled_tracker
584 590 issue = Issue.find(1)
585 591 target = Project.find(2)
586 592 target.tracker_ids = [3]
587 593 target.save
588 assert_equal false, issue.move_to_project(target)
594 issue.project = target
595 assert issue.save
589 596 issue.reload
590 assert_equal 1, issue.project_id
597 assert_equal 2, issue.project_id
598 assert_equal 3, issue.tracker_id
591 599 end
592 600
593 601 def test_copy_to_the_same_project
594 602 issue = Issue.find(1)
595 copy = nil
603 copy = issue.copy
596 604 assert_difference 'Issue.count' do
597 copy = issue.move_to_project(issue.project, nil, :copy => true)
605 copy.save!
598 606 end
599 607 assert_kind_of Issue, copy
600 608 assert_equal issue.project, copy.project
601 609 assert_equal "125", copy.custom_value_for(2).value
602 610 end
603 611
604 612 def test_copy_to_another_project_and_tracker
605 613 issue = Issue.find(1)
606 copy = nil
614 copy = issue.copy(:project_id => 3, :tracker_id => 2)
607 615 assert_difference 'Issue.count' do
608 copy = issue.move_to_project(Project.find(3), Tracker.find(2), :copy => true)
616 copy.save!
609 617 end
610 618 copy.reload
611 619 assert_kind_of Issue, copy
612 620 assert_equal Project.find(3), copy.project
613 621 assert_equal Tracker.find(2), copy.tracker
614 622 # Custom field #2 is not associated with target tracker
615 623 assert_nil copy.custom_value_for(2)
616 624 end
617 625
618 context "#move_to_project" do
619 context "as a copy" do
620 setup do
621 @issue = Issue.find(1)
622 @copy = nil
623 end
624
625 should "not create a journal" do
626 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:assigned_to_id => 3}})
627 assert_equal 0, @copy.reload.journals.size
628 end
629
630 should "allow assigned_to changes" do
631 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:assigned_to_id => 3}})
632 assert_equal 3, @copy.assigned_to_id
633 end
634
635 should "allow status changes" do
636 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:status_id => 2}})
637 assert_equal 2, @copy.status_id
638 end
626 context "#copy" do
627 setup do
628 @issue = Issue.find(1)
629 end
639 630
640 should "allow start date changes" do
641 date = Date.today
642 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:start_date => date}})
643 assert_equal date, @copy.start_date
644 end
631 should "not create a journal" do
632 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :assigned_to_id => 3)
633 copy.save!
634 assert_equal 0, copy.reload.journals.size
635 end
645 636
646 should "allow due date changes" do
647 date = Date.today
648 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:due_date => date}})
637 should "allow assigned_to changes" do
638 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :assigned_to_id => 3)
639 assert_equal 3, copy.assigned_to_id
640 end
649 641
650 assert_equal date, @copy.due_date
651 end
642 should "allow status changes" do
643 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :status_id => 2)
644 assert_equal 2, copy.status_id
645 end
652 646
653 should "set current user as author" do
654 User.current = User.find(9)
655 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {}})
647 should "allow start date changes" do
648 date = Date.today
649 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :start_date => date)
650 assert_equal date, copy.start_date
651 end
656 652
657 assert_equal User.current, @copy.author
658 end
653 should "allow due date changes" do
654 date = Date.today
655 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :due_date => date)
656 assert_equal date, copy.due_date
657 end
659 658
660 should "create a journal with notes" do
661 date = Date.today
662 notes = "Notes added when copying"
663 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :notes => notes, :attributes => {:start_date => date}})
659 should "set current user as author" do
660 User.current = User.find(9)
661 copy = @issue.copy(:project_id => 3, :tracker_id => 2)
662 assert_equal User.current, copy.author
663 end
664 664
665 assert_equal 1, @copy.journals.size
666 journal = @copy.journals.first
667 assert_equal 0, journal.details.size
668 assert_equal notes, journal.notes
669 end
665 should "create a journal with notes" do
666 date = Date.today
667 notes = "Notes added when copying"
668 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :start_date => date)
669 copy.init_journal(User.current, notes)
670 copy.save!
671
672 assert_equal 1, copy.journals.size
673 journal = copy.journals.first
674 assert_equal 0, journal.details.size
675 assert_equal notes, journal.notes
670 676 end
671 677 end
672 678
673 679 def test_recipients_should_not_include_users_that_cannot_view_the_issue
674 680 issue = Issue.find(12)
675 681 assert issue.recipients.include?(issue.author.mail)
676 # move the issue to a private project
677 copy = issue.move_to_project(Project.find(5), Tracker.find(2), :copy => true)
682 # copy the issue to a private project
683 copy = issue.copy(:project_id => 5, :tracker_id => 2)
678 684 # author is not a member of project anymore
679 685 assert !copy.recipients.include?(copy.author.mail)
680 686 end
681 687
682 688 def test_recipients_should_include_the_assigned_group_members
683 689 group_member = User.generate_with_protected!
684 690 group = Group.generate!
685 691 group.users << group_member
686 692
687 693 issue = Issue.find(12)
688 694 issue.assigned_to = group
689 695 assert issue.recipients.include?(group_member.mail)
690 696 end
691 697
692 698 def test_watcher_recipients_should_not_include_users_that_cannot_view_the_issue
693 699 user = User.find(3)
694 700 issue = Issue.find(9)
695 701 Watcher.create!(:user => user, :watchable => issue)
696 702 assert issue.watched_by?(user)
697 703 assert !issue.watcher_recipients.include?(user.mail)
698 704 end
699 705
700 706 def test_issue_destroy
701 707 Issue.find(1).destroy
702 708 assert_nil Issue.find_by_id(1)
703 709 assert_nil TimeEntry.find_by_issue_id(1)
704 710 end
705 711
706 712 def test_blocked
707 713 blocked_issue = Issue.find(9)
708 714 blocking_issue = Issue.find(10)
709 715
710 716 assert blocked_issue.blocked?
711 717 assert !blocking_issue.blocked?
712 718 end
713 719
714 720 def test_blocked_issues_dont_allow_closed_statuses
715 721 blocked_issue = Issue.find(9)
716 722
717 723 allowed_statuses = blocked_issue.new_statuses_allowed_to(users(:users_002))
718 724 assert !allowed_statuses.empty?
719 725 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
720 726 assert closed_statuses.empty?
721 727 end
722 728
723 729 def test_unblocked_issues_allow_closed_statuses
724 730 blocking_issue = Issue.find(10)
725 731
726 732 allowed_statuses = blocking_issue.new_statuses_allowed_to(users(:users_002))
727 733 assert !allowed_statuses.empty?
728 734 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
729 735 assert !closed_statuses.empty?
730 736 end
731 737
732 738 def test_rescheduling_an_issue_should_reschedule_following_issue
733 739 issue1 = Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => '-', :start_date => Date.today, :due_date => Date.today + 2)
734 740 issue2 = Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => '-', :start_date => Date.today, :due_date => Date.today + 2)
735 741 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_PRECEDES)
736 742 assert_equal issue1.due_date + 1, issue2.reload.start_date
737 743
738 744 issue1.due_date = Date.today + 5
739 745 issue1.save!
740 746 assert_equal issue1.due_date + 1, issue2.reload.start_date
741 747 end
742 748
743 749 def test_overdue
744 750 assert Issue.new(:due_date => 1.day.ago.to_date).overdue?
745 751 assert !Issue.new(:due_date => Date.today).overdue?
746 752 assert !Issue.new(:due_date => 1.day.from_now.to_date).overdue?
747 753 assert !Issue.new(:due_date => nil).overdue?
748 754 assert !Issue.new(:due_date => 1.day.ago.to_date, :status => IssueStatus.find(:first, :conditions => {:is_closed => true})).overdue?
749 755 end
750 756
751 757 context "#behind_schedule?" do
752 758 should "be false if the issue has no start_date" do
753 759 assert !Issue.new(:start_date => nil, :due_date => 1.day.from_now.to_date, :done_ratio => 0).behind_schedule?
754 760 end
755 761
756 762 should "be false if the issue has no end_date" do
757 763 assert !Issue.new(:start_date => 1.day.from_now.to_date, :due_date => nil, :done_ratio => 0).behind_schedule?
758 764 end
759 765
760 766 should "be false if the issue has more done than it's calendar time" do
761 767 assert !Issue.new(:start_date => 50.days.ago.to_date, :due_date => 50.days.from_now.to_date, :done_ratio => 90).behind_schedule?
762 768 end
763 769
764 770 should "be true if the issue hasn't been started at all" do
765 771 assert Issue.new(:start_date => 1.day.ago.to_date, :due_date => 1.day.from_now.to_date, :done_ratio => 0).behind_schedule?
766 772 end
767 773
768 774 should "be true if the issue has used more calendar time than it's done ratio" do
769 775 assert Issue.new(:start_date => 100.days.ago.to_date, :due_date => Date.today, :done_ratio => 90).behind_schedule?
770 776 end
771 777 end
772 778
773 779 context "#assignable_users" do
774 780 should "be Users" do
775 781 assert_kind_of User, Issue.find(1).assignable_users.first
776 782 end
777 783
778 784 should "include the issue author" do
779 785 project = Project.find(1)
780 786 non_project_member = User.generate!
781 787 issue = Issue.generate_for_project!(project, :author => non_project_member)
782 788
783 789 assert issue.assignable_users.include?(non_project_member)
784 790 end
785 791
786 792 should "include the current assignee" do
787 793 project = Project.find(1)
788 794 user = User.generate!
789 795 issue = Issue.generate_for_project!(project, :assigned_to => user)
790 796 user.lock!
791 797
792 798 assert Issue.find(issue.id).assignable_users.include?(user)
793 799 end
794 800
795 801 should "not show the issue author twice" do
796 802 assignable_user_ids = Issue.find(1).assignable_users.collect(&:id)
797 803 assert_equal 2, assignable_user_ids.length
798 804
799 805 assignable_user_ids.each do |user_id|
800 806 assert_equal 1, assignable_user_ids.select {|i| i == user_id}.length, "User #{user_id} appears more or less than once"
801 807 end
802 808 end
803 809
804 810 context "with issue_group_assignment" do
805 811 should "include groups" do
806 812 issue = Issue.new(:project => Project.find(2))
807 813
808 814 with_settings :issue_group_assignment => '1' do
809 815 assert_equal %w(Group User), issue.assignable_users.map {|a| a.class.name}.uniq.sort
810 816 assert issue.assignable_users.include?(Group.find(11))
811 817 end
812 818 end
813 819 end
814 820
815 821 context "without issue_group_assignment" do
816 822 should "not include groups" do
817 823 issue = Issue.new(:project => Project.find(2))
818 824
819 825 with_settings :issue_group_assignment => '0' do
820 826 assert_equal %w(User), issue.assignable_users.map {|a| a.class.name}.uniq.sort
821 827 assert !issue.assignable_users.include?(Group.find(11))
822 828 end
823 829 end
824 830 end
825 831 end
826 832
827 833 def test_create_should_send_email_notification
828 834 ActionMailer::Base.deliveries.clear
829 835 issue = Issue.new(:project_id => 1, :tracker_id => 1,
830 836 :author_id => 3, :status_id => 1,
831 837 :priority => IssuePriority.all.first,
832 838 :subject => 'test_create', :estimated_hours => '1:30')
833 839
834 840 assert issue.save
835 841 assert_equal 1, ActionMailer::Base.deliveries.size
836 842 end
837 843
838 844 def test_stale_issue_should_not_send_email_notification
839 845 ActionMailer::Base.deliveries.clear
840 846 issue = Issue.find(1)
841 847 stale = Issue.find(1)
842 848
843 849 issue.init_journal(User.find(1))
844 850 issue.subject = 'Subjet update'
845 851 assert issue.save
846 852 assert_equal 1, ActionMailer::Base.deliveries.size
847 853 ActionMailer::Base.deliveries.clear
848 854
849 855 stale.init_journal(User.find(1))
850 856 stale.subject = 'Another subjet update'
851 857 assert_raise ActiveRecord::StaleObjectError do
852 858 stale.save
853 859 end
854 860 assert ActionMailer::Base.deliveries.empty?
855 861 end
856 862
857 863 def test_journalized_description
858 864 IssueCustomField.delete_all
859 865
860 866 i = Issue.first
861 867 old_description = i.description
862 868 new_description = "This is the new description"
863 869
864 870 i.init_journal(User.find(2))
865 871 i.description = new_description
866 872 assert_difference 'Journal.count', 1 do
867 873 assert_difference 'JournalDetail.count', 1 do
868 874 i.save!
869 875 end
870 876 end
871 877
872 878 detail = JournalDetail.first(:order => 'id DESC')
873 879 assert_equal i, detail.journal.journalized
874 880 assert_equal 'attr', detail.property
875 881 assert_equal 'description', detail.prop_key
876 882 assert_equal old_description, detail.old_value
877 883 assert_equal new_description, detail.value
878 884 end
879 885
880 886 def test_blank_descriptions_should_not_be_journalized
881 887 IssueCustomField.delete_all
882 888 Issue.update_all("description = NULL", "id=1")
883 889
884 890 i = Issue.find(1)
885 891 i.init_journal(User.find(2))
886 892 i.subject = "blank description"
887 893 i.description = "\r\n"
888 894
889 895 assert_difference 'Journal.count', 1 do
890 896 assert_difference 'JournalDetail.count', 1 do
891 897 i.save!
892 898 end
893 899 end
894 900 end
895 901
896 902 def test_description_eol_should_be_normalized
897 903 i = Issue.new(:description => "CR \r LF \n CRLF \r\n")
898 904 assert_equal "CR \r\n LF \r\n CRLF \r\n", i.description
899 905 end
900 906
901 907 def test_saving_twice_should_not_duplicate_journal_details
902 908 i = Issue.find(:first)
903 909 i.init_journal(User.find(2), 'Some notes')
904 910 # initial changes
905 911 i.subject = 'New subject'
906 912 i.done_ratio = i.done_ratio + 10
907 913 assert_difference 'Journal.count' do
908 914 assert i.save
909 915 end
910 916 # 1 more change
911 917 i.priority = IssuePriority.find(:first, :conditions => ["id <> ?", i.priority_id])
912 918 assert_no_difference 'Journal.count' do
913 919 assert_difference 'JournalDetail.count', 1 do
914 920 i.save
915 921 end
916 922 end
917 923 # no more change
918 924 assert_no_difference 'Journal.count' do
919 925 assert_no_difference 'JournalDetail.count' do
920 926 i.save
921 927 end
922 928 end
923 929 end
924 930
925 931 def test_all_dependent_issues
926 932 IssueRelation.delete_all
927 933 assert IssueRelation.create!(:issue_from => Issue.find(1),
928 934 :issue_to => Issue.find(2),
929 935 :relation_type => IssueRelation::TYPE_PRECEDES)
930 936 assert IssueRelation.create!(:issue_from => Issue.find(2),
931 937 :issue_to => Issue.find(3),
932 938 :relation_type => IssueRelation::TYPE_PRECEDES)
933 939 assert IssueRelation.create!(:issue_from => Issue.find(3),
934 940 :issue_to => Issue.find(8),
935 941 :relation_type => IssueRelation::TYPE_PRECEDES)
936 942
937 943 assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort
938 944 end
939 945
940 946 def test_all_dependent_issues_with_persistent_circular_dependency
941 947 IssueRelation.delete_all
942 948 assert IssueRelation.create!(:issue_from => Issue.find(1),
943 949 :issue_to => Issue.find(2),
944 950 :relation_type => IssueRelation::TYPE_PRECEDES)
945 951 assert IssueRelation.create!(:issue_from => Issue.find(2),
946 952 :issue_to => Issue.find(3),
947 953 :relation_type => IssueRelation::TYPE_PRECEDES)
948 954 # Validation skipping
949 955 assert IssueRelation.new(:issue_from => Issue.find(3),
950 956 :issue_to => Issue.find(1),
951 957 :relation_type => IssueRelation::TYPE_PRECEDES).save(false)
952 958
953 959 assert_equal [2, 3], Issue.find(1).all_dependent_issues.collect(&:id).sort
954 960 end
955 961
956 962 def test_all_dependent_issues_with_persistent_multiple_circular_dependencies
957 963 IssueRelation.delete_all
958 964 assert IssueRelation.create!(:issue_from => Issue.find(1),
959 965 :issue_to => Issue.find(2),
960 966 :relation_type => IssueRelation::TYPE_RELATES)
961 967 assert IssueRelation.create!(:issue_from => Issue.find(2),
962 968 :issue_to => Issue.find(3),
963 969 :relation_type => IssueRelation::TYPE_RELATES)
964 970 assert IssueRelation.create!(:issue_from => Issue.find(3),
965 971 :issue_to => Issue.find(8),
966 972 :relation_type => IssueRelation::TYPE_RELATES)
967 973 # Validation skipping
968 974 assert IssueRelation.new(:issue_from => Issue.find(8),
969 975 :issue_to => Issue.find(2),
970 976 :relation_type => IssueRelation::TYPE_RELATES).save(false)
971 977 assert IssueRelation.new(:issue_from => Issue.find(3),
972 978 :issue_to => Issue.find(1),
973 979 :relation_type => IssueRelation::TYPE_RELATES).save(false)
974 980
975 981 assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort
976 982 end
977 983
978 984 context "#done_ratio" do
979 985 setup do
980 986 @issue = Issue.find(1)
981 987 @issue_status = IssueStatus.find(1)
982 988 @issue_status.update_attribute(:default_done_ratio, 50)
983 989 @issue2 = Issue.find(2)
984 990 @issue_status2 = IssueStatus.find(2)
985 991 @issue_status2.update_attribute(:default_done_ratio, 0)
986 992 end
987 993
988 994 teardown do
989 995 Setting.issue_done_ratio = 'issue_field'
990 996 end
991 997
992 998 context "with Setting.issue_done_ratio using the issue_field" do
993 999 setup do
994 1000 Setting.issue_done_ratio = 'issue_field'
995 1001 end
996 1002
997 1003 should "read the issue's field" do
998 1004 assert_equal 0, @issue.done_ratio
999 1005 assert_equal 30, @issue2.done_ratio
1000 1006 end
1001 1007 end
1002 1008
1003 1009 context "with Setting.issue_done_ratio using the issue_status" do
1004 1010 setup do
1005 1011 Setting.issue_done_ratio = 'issue_status'
1006 1012 end
1007 1013
1008 1014 should "read the Issue Status's default done ratio" do
1009 1015 assert_equal 50, @issue.done_ratio
1010 1016 assert_equal 0, @issue2.done_ratio
1011 1017 end
1012 1018 end
1013 1019 end
1014 1020
1015 1021 context "#update_done_ratio_from_issue_status" do
1016 1022 setup do
1017 1023 @issue = Issue.find(1)
1018 1024 @issue_status = IssueStatus.find(1)
1019 1025 @issue_status.update_attribute(:default_done_ratio, 50)
1020 1026 @issue2 = Issue.find(2)
1021 1027 @issue_status2 = IssueStatus.find(2)
1022 1028 @issue_status2.update_attribute(:default_done_ratio, 0)
1023 1029 end
1024 1030
1025 1031 context "with Setting.issue_done_ratio using the issue_field" do
1026 1032 setup do
1027 1033 Setting.issue_done_ratio = 'issue_field'
1028 1034 end
1029 1035
1030 1036 should "not change the issue" do
1031 1037 @issue.update_done_ratio_from_issue_status
1032 1038 @issue2.update_done_ratio_from_issue_status
1033 1039
1034 1040 assert_equal 0, @issue.read_attribute(:done_ratio)
1035 1041 assert_equal 30, @issue2.read_attribute(:done_ratio)
1036 1042 end
1037 1043 end
1038 1044
1039 1045 context "with Setting.issue_done_ratio using the issue_status" do
1040 1046 setup do
1041 1047 Setting.issue_done_ratio = 'issue_status'
1042 1048 end
1043 1049
1044 1050 should "change the issue's done ratio" do
1045 1051 @issue.update_done_ratio_from_issue_status
1046 1052 @issue2.update_done_ratio_from_issue_status
1047 1053
1048 1054 assert_equal 50, @issue.read_attribute(:done_ratio)
1049 1055 assert_equal 0, @issue2.read_attribute(:done_ratio)
1050 1056 end
1051 1057 end
1052 1058 end
1053 1059
1054 1060 test "#by_tracker" do
1055 1061 User.current = User.anonymous
1056 1062 groups = Issue.by_tracker(Project.find(1))
1057 1063 assert_equal 3, groups.size
1058 1064 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1059 1065 end
1060 1066
1061 1067 test "#by_version" do
1062 1068 User.current = User.anonymous
1063 1069 groups = Issue.by_version(Project.find(1))
1064 1070 assert_equal 3, groups.size
1065 1071 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1066 1072 end
1067 1073
1068 1074 test "#by_priority" do
1069 1075 User.current = User.anonymous
1070 1076 groups = Issue.by_priority(Project.find(1))
1071 1077 assert_equal 4, groups.size
1072 1078 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1073 1079 end
1074 1080
1075 1081 test "#by_category" do
1076 1082 User.current = User.anonymous
1077 1083 groups = Issue.by_category(Project.find(1))
1078 1084 assert_equal 2, groups.size
1079 1085 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1080 1086 end
1081 1087
1082 1088 test "#by_assigned_to" do
1083 1089 User.current = User.anonymous
1084 1090 groups = Issue.by_assigned_to(Project.find(1))
1085 1091 assert_equal 2, groups.size
1086 1092 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1087 1093 end
1088 1094
1089 1095 test "#by_author" do
1090 1096 User.current = User.anonymous
1091 1097 groups = Issue.by_author(Project.find(1))
1092 1098 assert_equal 4, groups.size
1093 1099 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1094 1100 end
1095 1101
1096 1102 test "#by_subproject" do
1097 1103 User.current = User.anonymous
1098 1104 groups = Issue.by_subproject(Project.find(1))
1099 1105 # Private descendant not visible
1100 1106 assert_equal 1, groups.size
1101 1107 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1102 1108 end
1103 1109
1104 1110 context ".allowed_target_projects_on_move" do
1105 1111 should "return all active projects for admin users" do
1106 1112 User.current = User.find(1)
1107 1113 assert_equal Project.active.count, Issue.allowed_target_projects_on_move.size
1108 1114 end
1109 1115
1110 1116 should "return allowed projects for non admin users" do
1111 1117 User.current = User.find(2)
1112 1118 Role.non_member.remove_permission! :move_issues
1113 1119 assert_equal 3, Issue.allowed_target_projects_on_move.size
1114 1120
1115 1121 Role.non_member.add_permission! :move_issues
1116 1122 assert_equal Project.active.count, Issue.allowed_target_projects_on_move.size
1117 1123 end
1118 1124 end
1119 1125
1120 1126 def test_recently_updated_with_limit_scopes
1121 1127 #should return the last updated issue
1122 1128 assert_equal 1, Issue.recently_updated.with_limit(1).length
1123 1129 assert_equal Issue.find(:first, :order => "updated_on DESC"), Issue.recently_updated.with_limit(1).first
1124 1130 end
1125 1131
1126 1132 def test_on_active_projects_scope
1127 1133 assert Project.find(2).archive
1128 1134
1129 1135 before = Issue.on_active_project.length
1130 1136 # test inclusion to results
1131 1137 issue = Issue.generate_for_project!(Project.find(1), :tracker => Project.find(2).trackers.first)
1132 1138 assert_equal before + 1, Issue.on_active_project.length
1133 1139
1134 1140 # Move to an archived project
1135 1141 issue.project = Project.find(2)
1136 1142 assert issue.save
1137 1143 assert_equal before, Issue.on_active_project.length
1138 1144 end
1139 1145
1140 1146 context "Issue#recipients" do
1141 1147 setup do
1142 1148 @project = Project.find(1)
1143 1149 @author = User.generate_with_protected!
1144 1150 @assignee = User.generate_with_protected!
1145 1151 @issue = Issue.generate_for_project!(@project, :assigned_to => @assignee, :author => @author)
1146 1152 end
1147 1153
1148 1154 should "include project recipients" do
1149 1155 assert @project.recipients.present?
1150 1156 @project.recipients.each do |project_recipient|
1151 1157 assert @issue.recipients.include?(project_recipient)
1152 1158 end
1153 1159 end
1154 1160
1155 1161 should "include the author if the author is active" do
1156 1162 assert @issue.author, "No author set for Issue"
1157 1163 assert @issue.recipients.include?(@issue.author.mail)
1158 1164 end
1159 1165
1160 1166 should "include the assigned to user if the assigned to user is active" do
1161 1167 assert @issue.assigned_to, "No assigned_to set for Issue"
1162 1168 assert @issue.recipients.include?(@issue.assigned_to.mail)
1163 1169 end
1164 1170
1165 1171 should "not include users who opt out of all email" do
1166 1172 @author.update_attribute(:mail_notification, :none)
1167 1173
1168 1174 assert !@issue.recipients.include?(@issue.author.mail)
1169 1175 end
1170 1176
1171 1177 should "not include the issue author if they are only notified of assigned issues" do
1172 1178 @author.update_attribute(:mail_notification, :only_assigned)
1173 1179
1174 1180 assert !@issue.recipients.include?(@issue.author.mail)
1175 1181 end
1176 1182
1177 1183 should "not include the assigned user if they are only notified of owned issues" do
1178 1184 @assignee.update_attribute(:mail_notification, :only_owner)
1179 1185
1180 1186 assert !@issue.recipients.include?(@issue.assigned_to.mail)
1181 1187 end
1182 1188
1183 1189 end
1184 1190 end
General Comments 0
You need to be logged in to leave comments. Login now