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