##// END OF EJS Templates
Code cleanup....
Jean-Philippe Lang -
r9996:0ce0b52342df
parent child
Show More
@@ -1,431 +1,426
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 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, :destroy]
23 before_filter :find_issues, :only => [:bulk_edit, :bulk_update, :destroy]
24 before_filter :find_project, :only => [:new, :create]
24 before_filter :find_project, :only => [:new, :create]
25 before_filter :authorize, :except => [:index]
25 before_filter :authorize, :except => [:index]
26 before_filter :find_optional_project, :only => [:index]
26 before_filter :find_optional_project, :only => [:index]
27 before_filter :check_for_default_issue_status, :only => [:new, :create]
27 before_filter :check_for_default_issue_status, :only => [:new, :create]
28 before_filter :build_new_issue_from_params, :only => [:new, :create]
28 before_filter :build_new_issue_from_params, :only => [:new, :create]
29 accept_rss_auth :index, :show
29 accept_rss_auth :index, :show
30 accept_api_auth :index, :show, :create, :update, :destroy
30 accept_api_auth :index, :show, :create, :update, :destroy
31
31
32 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
32 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
33
33
34 helper :journals
34 helper :journals
35 helper :projects
35 helper :projects
36 include ProjectsHelper
36 include ProjectsHelper
37 helper :custom_fields
37 helper :custom_fields
38 include CustomFieldsHelper
38 include CustomFieldsHelper
39 helper :issue_relations
39 helper :issue_relations
40 include IssueRelationsHelper
40 include IssueRelationsHelper
41 helper :watchers
41 helper :watchers
42 include WatchersHelper
42 include WatchersHelper
43 helper :attachments
43 helper :attachments
44 include AttachmentsHelper
44 include AttachmentsHelper
45 helper :queries
45 helper :queries
46 include QueriesHelper
46 include QueriesHelper
47 helper :repositories
47 helper :repositories
48 include RepositoriesHelper
48 include RepositoriesHelper
49 helper :sort
49 helper :sort
50 include SortHelper
50 include SortHelper
51 include IssuesHelper
51 include IssuesHelper
52 helper :timelog
52 helper :timelog
53 include Redmine::Export::PDF
53 include Redmine::Export::PDF
54
54
55 def index
55 def index
56 retrieve_query
56 retrieve_query
57 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
57 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
58 sort_update(@query.sortable_columns)
58 sort_update(@query.sortable_columns)
59
59
60 if @query.valid?
60 if @query.valid?
61 case params[:format]
61 case params[:format]
62 when 'csv', 'pdf'
62 when 'csv', 'pdf'
63 @limit = Setting.issues_export_limit.to_i
63 @limit = Setting.issues_export_limit.to_i
64 when 'atom'
64 when 'atom'
65 @limit = Setting.feeds_limit.to_i
65 @limit = Setting.feeds_limit.to_i
66 when 'xml', 'json'
66 when 'xml', 'json'
67 @offset, @limit = api_offset_and_limit
67 @offset, @limit = api_offset_and_limit
68 else
68 else
69 @limit = per_page_option
69 @limit = per_page_option
70 end
70 end
71
71
72 @issue_count = @query.issue_count
72 @issue_count = @query.issue_count
73 @issue_pages = Paginator.new self, @issue_count, @limit, params['page']
73 @issue_pages = Paginator.new self, @issue_count, @limit, params['page']
74 @offset ||= @issue_pages.current.offset
74 @offset ||= @issue_pages.current.offset
75 @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
75 @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
76 :order => sort_clause,
76 :order => sort_clause,
77 :offset => @offset,
77 :offset => @offset,
78 :limit => @limit)
78 :limit => @limit)
79 @issue_count_by_group = @query.issue_count_by_group
79 @issue_count_by_group = @query.issue_count_by_group
80
80
81 respond_to do |format|
81 respond_to do |format|
82 format.html { render :template => 'issues/index', :layout => !request.xhr? }
82 format.html { render :template => 'issues/index', :layout => !request.xhr? }
83 format.api {
83 format.api {
84 Issue.load_relations(@issues) if include_in_api_response?('relations')
84 Issue.load_relations(@issues) if include_in_api_response?('relations')
85 }
85 }
86 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
86 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
87 format.csv { send_data(issues_to_csv(@issues, @project, @query, params), :type => 'text/csv; header=present', :filename => 'export.csv') }
87 format.csv { send_data(issues_to_csv(@issues, @project, @query, params), :type => 'text/csv; header=present', :filename => 'export.csv') }
88 format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'export.pdf') }
88 format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'export.pdf') }
89 end
89 end
90 else
90 else
91 respond_to do |format|
91 respond_to do |format|
92 format.html { render(:template => 'issues/index', :layout => !request.xhr?) }
92 format.html { render(:template => 'issues/index', :layout => !request.xhr?) }
93 format.any(:atom, :csv, :pdf) { render(:nothing => true) }
93 format.any(:atom, :csv, :pdf) { render(:nothing => true) }
94 format.api { render_validation_errors(@query) }
94 format.api { render_validation_errors(@query) }
95 end
95 end
96 end
96 end
97 rescue ActiveRecord::RecordNotFound
97 rescue ActiveRecord::RecordNotFound
98 render_404
98 render_404
99 end
99 end
100
100
101 def show
101 def show
102 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
102 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
103 @journals.each_with_index {|j,i| j.indice = i+1}
103 @journals.each_with_index {|j,i| j.indice = i+1}
104 @journals.reverse! if User.current.wants_comments_in_reverse_order?
104 @journals.reverse! if User.current.wants_comments_in_reverse_order?
105
105
106 @changesets = @issue.changesets.visible.all
106 @changesets = @issue.changesets.visible.all
107 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
107 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
108
108
109 @relations = @issue.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
109 @relations = @issue.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
110 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
110 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
111 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
111 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
112 @priorities = IssuePriority.active
112 @priorities = IssuePriority.active
113 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
113 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
114 respond_to do |format|
114 respond_to do |format|
115 format.html {
115 format.html {
116 retrieve_previous_and_next_issue_ids
116 retrieve_previous_and_next_issue_ids
117 render :template => 'issues/show'
117 render :template => 'issues/show'
118 }
118 }
119 format.api
119 format.api
120 format.atom { render :template => 'journals/index', :layout => false, :content_type => 'application/atom+xml' }
120 format.atom { render :template => 'journals/index', :layout => false, :content_type => 'application/atom+xml' }
121 format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
121 format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
122 end
122 end
123 end
123 end
124
124
125 # Add a new issue
125 # Add a new issue
126 # The new issue will be created from an existing one if copy_from parameter is given
126 # The new issue will be created from an existing one if copy_from parameter is given
127 def new
127 def new
128 respond_to do |format|
128 respond_to do |format|
129 format.html { render :action => 'new', :layout => !request.xhr? }
129 format.html { render :action => 'new', :layout => !request.xhr? }
130 format.js { render :partial => 'update_form' }
130 format.js { render :partial => 'update_form' }
131 end
131 end
132 end
132 end
133
133
134 def create
134 def create
135 call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
135 call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
136 @issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads]))
136 @issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads]))
137 if @issue.save
137 if @issue.save
138 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
138 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
139 respond_to do |format|
139 respond_to do |format|
140 format.html {
140 format.html {
141 render_attachment_warning_if_needed(@issue)
141 render_attachment_warning_if_needed(@issue)
142 flash[:notice] = l(:notice_issue_successful_create, :id => view_context.link_to("##{@issue.id}", issue_path(@issue)))
142 flash[:notice] = l(:notice_issue_successful_create, :id => view_context.link_to("##{@issue.id}", issue_path(@issue)))
143 redirect_to(params[:continue] ? { :action => 'new', :project_id => @issue.project, :issue => {:tracker_id => @issue.tracker, :parent_issue_id => @issue.parent_issue_id}.reject {|k,v| v.nil?} } :
143 redirect_to(params[:continue] ? { :action => 'new', :project_id => @issue.project, :issue => {:tracker_id => @issue.tracker, :parent_issue_id => @issue.parent_issue_id}.reject {|k,v| v.nil?} } :
144 { :action => 'show', :id => @issue })
144 { :action => 'show', :id => @issue })
145 }
145 }
146 format.api { render :action => 'show', :status => :created, :location => issue_url(@issue) }
146 format.api { render :action => 'show', :status => :created, :location => issue_url(@issue) }
147 end
147 end
148 return
148 return
149 else
149 else
150 respond_to do |format|
150 respond_to do |format|
151 format.html { render :action => 'new' }
151 format.html { render :action => 'new' }
152 format.api { render_validation_errors(@issue) }
152 format.api { render_validation_errors(@issue) }
153 end
153 end
154 end
154 end
155 end
155 end
156
156
157 def edit
157 def edit
158 return unless update_issue_from_params
158 return unless update_issue_from_params
159
159
160 respond_to do |format|
160 respond_to do |format|
161 format.html { }
161 format.html { }
162 format.xml { }
162 format.xml { }
163 end
163 end
164 end
164 end
165
165
166 def update
166 def update
167 return unless update_issue_from_params
167 return unless update_issue_from_params
168 @issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads]))
168 @issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads]))
169 saved = false
169 saved = false
170 begin
170 begin
171 saved = @issue.save_issue_with_child_records(params, @time_entry)
171 saved = @issue.save_issue_with_child_records(params, @time_entry)
172 rescue ActiveRecord::StaleObjectError
172 rescue ActiveRecord::StaleObjectError
173 @conflict = true
173 @conflict = true
174 if params[:last_journal_id]
174 if params[:last_journal_id]
175 if params[:last_journal_id].present?
175 @conflict_journals = @issue.journals_after(params[:last_journal_id]).all
176 last_journal_id = params[:last_journal_id].to_i
177 @conflict_journals = @issue.journals.all(:conditions => ["#{Journal.table_name}.id > ?", last_journal_id])
178 else
179 @conflict_journals = @issue.journals.all
180 end
181 end
176 end
182 end
177 end
183
178
184 if saved
179 if saved
185 render_attachment_warning_if_needed(@issue)
180 render_attachment_warning_if_needed(@issue)
186 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
181 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
187
182
188 respond_to do |format|
183 respond_to do |format|
189 format.html { redirect_back_or_default({:action => 'show', :id => @issue}) }
184 format.html { redirect_back_or_default({:action => 'show', :id => @issue}) }
190 format.api { render_api_ok }
185 format.api { render_api_ok }
191 end
186 end
192 else
187 else
193 respond_to do |format|
188 respond_to do |format|
194 format.html { render :action => 'edit' }
189 format.html { render :action => 'edit' }
195 format.api { render_validation_errors(@issue) }
190 format.api { render_validation_errors(@issue) }
196 end
191 end
197 end
192 end
198 end
193 end
199
194
200 # Bulk edit/copy a set of issues
195 # Bulk edit/copy a set of issues
201 def bulk_edit
196 def bulk_edit
202 @issues.sort!
197 @issues.sort!
203 @copy = params[:copy].present?
198 @copy = params[:copy].present?
204 @notes = params[:notes]
199 @notes = params[:notes]
205
200
206 if User.current.allowed_to?(:move_issues, @projects)
201 if User.current.allowed_to?(:move_issues, @projects)
207 @allowed_projects = Issue.allowed_target_projects_on_move
202 @allowed_projects = Issue.allowed_target_projects_on_move
208 if params[:issue]
203 if params[:issue]
209 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:issue][:project_id].to_s}
204 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:issue][:project_id].to_s}
210 if @target_project
205 if @target_project
211 target_projects = [@target_project]
206 target_projects = [@target_project]
212 end
207 end
213 end
208 end
214 end
209 end
215 target_projects ||= @projects
210 target_projects ||= @projects
216
211
217 if @copy
212 if @copy
218 @available_statuses = [IssueStatus.default]
213 @available_statuses = [IssueStatus.default]
219 else
214 else
220 @available_statuses = @issues.map(&:new_statuses_allowed_to).reduce(:&)
215 @available_statuses = @issues.map(&:new_statuses_allowed_to).reduce(:&)
221 end
216 end
222 @custom_fields = target_projects.map{|p|p.all_issue_custom_fields}.reduce(:&)
217 @custom_fields = target_projects.map{|p|p.all_issue_custom_fields}.reduce(:&)
223 @assignables = target_projects.map(&:assignable_users).reduce(:&)
218 @assignables = target_projects.map(&:assignable_users).reduce(:&)
224 @trackers = target_projects.map(&:trackers).reduce(:&)
219 @trackers = target_projects.map(&:trackers).reduce(:&)
225 @versions = target_projects.map {|p| p.shared_versions.open}.reduce(:&)
220 @versions = target_projects.map {|p| p.shared_versions.open}.reduce(:&)
226 @categories = target_projects.map {|p| p.issue_categories}.reduce(:&)
221 @categories = target_projects.map {|p| p.issue_categories}.reduce(:&)
227 if @copy
222 if @copy
228 @attachments_present = @issues.detect {|i| i.attachments.any?}.present?
223 @attachments_present = @issues.detect {|i| i.attachments.any?}.present?
229 end
224 end
230
225
231 @safe_attributes = @issues.map(&:safe_attribute_names).reduce(:&)
226 @safe_attributes = @issues.map(&:safe_attribute_names).reduce(:&)
232 render :layout => false if request.xhr?
227 render :layout => false if request.xhr?
233 end
228 end
234
229
235 def bulk_update
230 def bulk_update
236 @issues.sort!
231 @issues.sort!
237 @copy = params[:copy].present?
232 @copy = params[:copy].present?
238 attributes = parse_params_for_bulk_issue_attributes(params)
233 attributes = parse_params_for_bulk_issue_attributes(params)
239
234
240 unsaved_issue_ids = []
235 unsaved_issue_ids = []
241 moved_issues = []
236 moved_issues = []
242 @issues.each do |issue|
237 @issues.each do |issue|
243 issue.reload
238 issue.reload
244 if @copy
239 if @copy
245 issue = issue.copy({}, :attachments => params[:copy_attachments].present?)
240 issue = issue.copy({}, :attachments => params[:copy_attachments].present?)
246 end
241 end
247 journal = issue.init_journal(User.current, params[:notes])
242 journal = issue.init_journal(User.current, params[:notes])
248 issue.safe_attributes = attributes
243 issue.safe_attributes = attributes
249 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
244 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
250 if issue.save
245 if issue.save
251 moved_issues << issue
246 moved_issues << issue
252 else
247 else
253 # Keep unsaved issue ids to display them in flash error
248 # Keep unsaved issue ids to display them in flash error
254 unsaved_issue_ids << issue.id
249 unsaved_issue_ids << issue.id
255 end
250 end
256 end
251 end
257 set_flash_from_bulk_issue_save(@issues, unsaved_issue_ids)
252 set_flash_from_bulk_issue_save(@issues, unsaved_issue_ids)
258
253
259 if params[:follow]
254 if params[:follow]
260 if @issues.size == 1 && moved_issues.size == 1
255 if @issues.size == 1 && moved_issues.size == 1
261 redirect_to :controller => 'issues', :action => 'show', :id => moved_issues.first
256 redirect_to :controller => 'issues', :action => 'show', :id => moved_issues.first
262 elsif moved_issues.map(&:project).uniq.size == 1
257 elsif moved_issues.map(&:project).uniq.size == 1
263 redirect_to :controller => 'issues', :action => 'index', :project_id => moved_issues.map(&:project).first
258 redirect_to :controller => 'issues', :action => 'index', :project_id => moved_issues.map(&:project).first
264 end
259 end
265 else
260 else
266 redirect_back_or_default({:controller => 'issues', :action => 'index', :project_id => @project})
261 redirect_back_or_default({:controller => 'issues', :action => 'index', :project_id => @project})
267 end
262 end
268 end
263 end
269
264
270 def destroy
265 def destroy
271 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
266 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
272 if @hours > 0
267 if @hours > 0
273 case params[:todo]
268 case params[:todo]
274 when 'destroy'
269 when 'destroy'
275 # nothing to do
270 # nothing to do
276 when 'nullify'
271 when 'nullify'
277 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
272 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
278 when 'reassign'
273 when 'reassign'
279 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
274 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
280 if reassign_to.nil?
275 if reassign_to.nil?
281 flash.now[:error] = l(:error_issue_not_found_in_project)
276 flash.now[:error] = l(:error_issue_not_found_in_project)
282 return
277 return
283 else
278 else
284 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
279 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
285 end
280 end
286 else
281 else
287 # display the destroy form if it's a user request
282 # display the destroy form if it's a user request
288 return unless api_request?
283 return unless api_request?
289 end
284 end
290 end
285 end
291 @issues.each do |issue|
286 @issues.each do |issue|
292 begin
287 begin
293 issue.reload.destroy
288 issue.reload.destroy
294 rescue ::ActiveRecord::RecordNotFound # raised by #reload if issue no longer exists
289 rescue ::ActiveRecord::RecordNotFound # raised by #reload if issue no longer exists
295 # nothing to do, issue was already deleted (eg. by a parent)
290 # nothing to do, issue was already deleted (eg. by a parent)
296 end
291 end
297 end
292 end
298 respond_to do |format|
293 respond_to do |format|
299 format.html { redirect_back_or_default(:action => 'index', :project_id => @project) }
294 format.html { redirect_back_or_default(:action => 'index', :project_id => @project) }
300 format.api { render_api_ok }
295 format.api { render_api_ok }
301 end
296 end
302 end
297 end
303
298
304 private
299 private
305 def find_issue
300 def find_issue
306 # Issue.visible.find(...) can not be used to redirect user to the login form
301 # Issue.visible.find(...) can not be used to redirect user to the login form
307 # if the issue actually exists but requires authentication
302 # if the issue actually exists but requires authentication
308 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
303 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
309 unless @issue.visible?
304 unless @issue.visible?
310 deny_access
305 deny_access
311 return
306 return
312 end
307 end
313 @project = @issue.project
308 @project = @issue.project
314 rescue ActiveRecord::RecordNotFound
309 rescue ActiveRecord::RecordNotFound
315 render_404
310 render_404
316 end
311 end
317
312
318 def find_project
313 def find_project
319 project_id = params[:project_id] || (params[:issue] && params[:issue][:project_id])
314 project_id = params[:project_id] || (params[:issue] && params[:issue][:project_id])
320 @project = Project.find(project_id)
315 @project = Project.find(project_id)
321 rescue ActiveRecord::RecordNotFound
316 rescue ActiveRecord::RecordNotFound
322 render_404
317 render_404
323 end
318 end
324
319
325 def retrieve_previous_and_next_issue_ids
320 def retrieve_previous_and_next_issue_ids
326 retrieve_query_from_session
321 retrieve_query_from_session
327 if @query
322 if @query
328 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
323 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
329 sort_update(@query.sortable_columns, 'issues_index_sort')
324 sort_update(@query.sortable_columns, 'issues_index_sort')
330 limit = 500
325 limit = 500
331 issue_ids = @query.issue_ids(:order => sort_clause, :limit => (limit + 1), :include => [:assigned_to, :tracker, :priority, :category, :fixed_version])
326 issue_ids = @query.issue_ids(:order => sort_clause, :limit => (limit + 1), :include => [:assigned_to, :tracker, :priority, :category, :fixed_version])
332 if (idx = issue_ids.index(@issue.id)) && idx < limit
327 if (idx = issue_ids.index(@issue.id)) && idx < limit
333 if issue_ids.size < 500
328 if issue_ids.size < 500
334 @issue_position = idx + 1
329 @issue_position = idx + 1
335 @issue_count = issue_ids.size
330 @issue_count = issue_ids.size
336 end
331 end
337 @prev_issue_id = issue_ids[idx - 1] if idx > 0
332 @prev_issue_id = issue_ids[idx - 1] if idx > 0
338 @next_issue_id = issue_ids[idx + 1] if idx < (issue_ids.size - 1)
333 @next_issue_id = issue_ids[idx + 1] if idx < (issue_ids.size - 1)
339 end
334 end
340 end
335 end
341 end
336 end
342
337
343 # Used by #edit and #update to set some common instance variables
338 # Used by #edit and #update to set some common instance variables
344 # from the params
339 # from the params
345 # TODO: Refactor, not everything in here is needed by #edit
340 # TODO: Refactor, not everything in here is needed by #edit
346 def update_issue_from_params
341 def update_issue_from_params
347 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
342 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
348 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
343 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
349 @time_entry.attributes = params[:time_entry]
344 @time_entry.attributes = params[:time_entry]
350
345
351 @notes = params[:notes] || (params[:issue].present? ? params[:issue][:notes] : nil)
346 @notes = params[:notes] || (params[:issue].present? ? params[:issue][:notes] : nil)
352 @issue.init_journal(User.current, @notes)
347 @issue.init_journal(User.current, @notes)
353
348
354 issue_attributes = params[:issue]
349 issue_attributes = params[:issue]
355 if issue_attributes && params[:conflict_resolution]
350 if issue_attributes && params[:conflict_resolution]
356 case params[:conflict_resolution]
351 case params[:conflict_resolution]
357 when 'overwrite'
352 when 'overwrite'
358 issue_attributes = issue_attributes.dup
353 issue_attributes = issue_attributes.dup
359 issue_attributes.delete(:lock_version)
354 issue_attributes.delete(:lock_version)
360 when 'add_notes'
355 when 'add_notes'
361 issue_attributes = {}
356 issue_attributes = {}
362 when 'cancel'
357 when 'cancel'
363 redirect_to issue_path(@issue)
358 redirect_to issue_path(@issue)
364 return false
359 return false
365 end
360 end
366 end
361 end
367 @issue.safe_attributes = issue_attributes
362 @issue.safe_attributes = issue_attributes
368 @priorities = IssuePriority.active
363 @priorities = IssuePriority.active
369 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
364 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
370 true
365 true
371 end
366 end
372
367
373 # TODO: Refactor, lots of extra code in here
368 # TODO: Refactor, lots of extra code in here
374 # TODO: Changing tracker on an existing issue should not trigger this
369 # TODO: Changing tracker on an existing issue should not trigger this
375 def build_new_issue_from_params
370 def build_new_issue_from_params
376 if params[:id].blank?
371 if params[:id].blank?
377 @issue = Issue.new
372 @issue = Issue.new
378 if params[:copy_from]
373 if params[:copy_from]
379 begin
374 begin
380 @copy_from = Issue.visible.find(params[:copy_from])
375 @copy_from = Issue.visible.find(params[:copy_from])
381 @copy_attachments = params[:copy_attachments].present? || request.get?
376 @copy_attachments = params[:copy_attachments].present? || request.get?
382 @issue.copy_from(@copy_from, :attachments => @copy_attachments)
377 @issue.copy_from(@copy_from, :attachments => @copy_attachments)
383 rescue ActiveRecord::RecordNotFound
378 rescue ActiveRecord::RecordNotFound
384 render_404
379 render_404
385 return
380 return
386 end
381 end
387 end
382 end
388 @issue.project = @project
383 @issue.project = @project
389 else
384 else
390 @issue = @project.issues.visible.find(params[:id])
385 @issue = @project.issues.visible.find(params[:id])
391 end
386 end
392
387
393 @issue.project = @project
388 @issue.project = @project
394 @issue.author = User.current
389 @issue.author = User.current
395 # Tracker must be set before custom field values
390 # Tracker must be set before custom field values
396 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
391 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
397 if @issue.tracker.nil?
392 if @issue.tracker.nil?
398 render_error l(:error_no_tracker_in_project)
393 render_error l(:error_no_tracker_in_project)
399 return false
394 return false
400 end
395 end
401 @issue.start_date ||= Date.today if Setting.default_issue_start_date_to_creation_date?
396 @issue.start_date ||= Date.today if Setting.default_issue_start_date_to_creation_date?
402 @issue.safe_attributes = params[:issue]
397 @issue.safe_attributes = params[:issue]
403
398
404 @priorities = IssuePriority.active
399 @priorities = IssuePriority.active
405 @allowed_statuses = @issue.new_statuses_allowed_to(User.current, true)
400 @allowed_statuses = @issue.new_statuses_allowed_to(User.current, true)
406 @available_watchers = (@issue.project.users.sort + @issue.watcher_users).uniq
401 @available_watchers = (@issue.project.users.sort + @issue.watcher_users).uniq
407 end
402 end
408
403
409 def check_for_default_issue_status
404 def check_for_default_issue_status
410 if IssueStatus.default.nil?
405 if IssueStatus.default.nil?
411 render_error l(:error_no_default_issue_status)
406 render_error l(:error_no_default_issue_status)
412 return false
407 return false
413 end
408 end
414 end
409 end
415
410
416 def parse_params_for_bulk_issue_attributes(params)
411 def parse_params_for_bulk_issue_attributes(params)
417 attributes = (params[:issue] || {}).reject {|k,v| v.blank?}
412 attributes = (params[:issue] || {}).reject {|k,v| v.blank?}
418 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
413 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
419 if custom = attributes[:custom_field_values]
414 if custom = attributes[:custom_field_values]
420 custom.reject! {|k,v| v.blank?}
415 custom.reject! {|k,v| v.blank?}
421 custom.keys.each do |k|
416 custom.keys.each do |k|
422 if custom[k].is_a?(Array)
417 if custom[k].is_a?(Array)
423 custom[k] << '' if custom[k].delete('__none__')
418 custom[k] << '' if custom[k].delete('__none__')
424 else
419 else
425 custom[k] = '' if custom[k] == '__none__'
420 custom[k] = '' if custom[k] == '__none__'
426 end
421 end
427 end
422 end
428 end
423 end
429 attributes
424 attributes
430 end
425 end
431 end
426 end
@@ -1,1241 +1,1250
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 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, :validate_required_fields
61 validate :validate_issue, :validate_required_fields
62
62
63 scope :visible,
63 scope :visible,
64 lambda {|*args| { :include => :project,
64 lambda {|*args| { :include => :project,
65 :conditions => Issue.visible_condition(args.shift || User.current, *args) } }
65 :conditions => Issue.visible_condition(args.shift || User.current, *args) } }
66
66
67 class << self; undef :open; end
67 class << self; undef :open; end
68 scope :open, lambda {|*args|
68 scope :open, lambda {|*args|
69 is_closed = args.size > 0 ? !args.first : false
69 is_closed = args.size > 0 ? !args.first : false
70 {:conditions => ["#{IssueStatus.table_name}.is_closed = ?", is_closed], :include => :status}
70 {:conditions => ["#{IssueStatus.table_name}.is_closed = ?", is_closed], :include => :status}
71 }
71 }
72
72
73 scope :recently_updated, :order => "#{Issue.table_name}.updated_on DESC"
73 scope :recently_updated, :order => "#{Issue.table_name}.updated_on DESC"
74 scope :with_limit, lambda { |limit| { :limit => limit} }
74 scope :with_limit, lambda { |limit| { :limit => limit} }
75 scope :on_active_project, :include => [:status, :project, :tracker],
75 scope :on_active_project, :include => [:status, :project, :tracker],
76 :conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"]
76 :conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"]
77
77
78 before_create :default_assign
78 before_create :default_assign
79 before_save :close_duplicates, :update_done_ratio_from_issue_status, :force_updated_on_change
79 before_save :close_duplicates, :update_done_ratio_from_issue_status, :force_updated_on_change
80 after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?}
80 after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?}
81 after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal
81 after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal
82 after_destroy :update_parent_attributes
82 after_destroy :update_parent_attributes
83
83
84 # Returns a SQL conditions string used to find all issues visible by the specified user
84 # Returns a SQL conditions string used to find all issues visible by the specified user
85 def self.visible_condition(user, options={})
85 def self.visible_condition(user, options={})
86 Project.allowed_to_condition(user, :view_issues, options) do |role, user|
86 Project.allowed_to_condition(user, :view_issues, options) do |role, user|
87 case role.issues_visibility
87 case role.issues_visibility
88 when 'all'
88 when 'all'
89 nil
89 nil
90 when 'default'
90 when 'default'
91 user_ids = [user.id] + user.groups.map(&:id)
91 user_ids = [user.id] + user.groups.map(&:id)
92 "(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
92 "(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
93 when 'own'
93 when 'own'
94 user_ids = [user.id] + user.groups.map(&:id)
94 user_ids = [user.id] + user.groups.map(&:id)
95 "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
95 "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
96 else
96 else
97 '1=0'
97 '1=0'
98 end
98 end
99 end
99 end
100 end
100 end
101
101
102 # Returns true if usr or current user is allowed to view the issue
102 # Returns true if usr or current user is allowed to view the issue
103 def visible?(usr=nil)
103 def visible?(usr=nil)
104 (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
104 (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
105 case role.issues_visibility
105 case role.issues_visibility
106 when 'all'
106 when 'all'
107 true
107 true
108 when 'default'
108 when 'default'
109 !self.is_private? || self.author == user || user.is_or_belongs_to?(assigned_to)
109 !self.is_private? || self.author == user || user.is_or_belongs_to?(assigned_to)
110 when 'own'
110 when 'own'
111 self.author == user || user.is_or_belongs_to?(assigned_to)
111 self.author == user || user.is_or_belongs_to?(assigned_to)
112 else
112 else
113 false
113 false
114 end
114 end
115 end
115 end
116 end
116 end
117
117
118 def initialize(attributes=nil, *args)
118 def initialize(attributes=nil, *args)
119 super
119 super
120 if new_record?
120 if new_record?
121 # set default values for new records only
121 # set default values for new records only
122 self.status ||= IssueStatus.default
122 self.status ||= IssueStatus.default
123 self.priority ||= IssuePriority.default
123 self.priority ||= IssuePriority.default
124 self.watcher_user_ids = []
124 self.watcher_user_ids = []
125 end
125 end
126 end
126 end
127
127
128 # AR#Persistence#destroy would raise and RecordNotFound exception
128 # AR#Persistence#destroy would raise and RecordNotFound exception
129 # if the issue was already deleted or updated (non matching lock_version).
129 # if the issue was already deleted or updated (non matching lock_version).
130 # This is a problem when bulk deleting issues or deleting a project
130 # This is a problem when bulk deleting issues or deleting a project
131 # (because an issue may already be deleted if its parent was deleted
131 # (because an issue may already be deleted if its parent was deleted
132 # first).
132 # first).
133 # The issue is reloaded by the nested_set before being deleted so
133 # The issue is reloaded by the nested_set before being deleted so
134 # the lock_version condition should not be an issue but we handle it.
134 # the lock_version condition should not be an issue but we handle it.
135 def destroy
135 def destroy
136 super
136 super
137 rescue ActiveRecord::RecordNotFound
137 rescue ActiveRecord::RecordNotFound
138 # Stale or already deleted
138 # Stale or already deleted
139 begin
139 begin
140 reload
140 reload
141 rescue ActiveRecord::RecordNotFound
141 rescue ActiveRecord::RecordNotFound
142 # The issue was actually already deleted
142 # The issue was actually already deleted
143 @destroyed = true
143 @destroyed = true
144 return freeze
144 return freeze
145 end
145 end
146 # The issue was stale, retry to destroy
146 # The issue was stale, retry to destroy
147 super
147 super
148 end
148 end
149
149
150 def reload(*args)
150 def reload(*args)
151 @workflow_rule_by_attribute = nil
151 @workflow_rule_by_attribute = nil
152 @assignable_versions = nil
152 @assignable_versions = nil
153 super
153 super
154 end
154 end
155
155
156 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
156 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
157 def available_custom_fields
157 def available_custom_fields
158 (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields.all) : []
158 (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields.all) : []
159 end
159 end
160
160
161 # Copies attributes from another issue, arg can be an id or an Issue
161 # Copies attributes from another issue, arg can be an id or an Issue
162 def copy_from(arg, options={})
162 def copy_from(arg, options={})
163 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
163 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
164 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
164 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
165 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
165 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
166 self.status = issue.status
166 self.status = issue.status
167 self.author = User.current
167 self.author = User.current
168 unless options[:attachments] == false
168 unless options[:attachments] == false
169 self.attachments = issue.attachments.map do |attachement|
169 self.attachments = issue.attachments.map do |attachement|
170 attachement.copy(:container => self)
170 attachement.copy(:container => self)
171 end
171 end
172 end
172 end
173 @copied_from = issue
173 @copied_from = issue
174 self
174 self
175 end
175 end
176
176
177 # Returns an unsaved copy of the issue
177 # Returns an unsaved copy of the issue
178 def copy(attributes=nil, copy_options={})
178 def copy(attributes=nil, copy_options={})
179 copy = self.class.new.copy_from(self, copy_options)
179 copy = self.class.new.copy_from(self, copy_options)
180 copy.attributes = attributes if attributes
180 copy.attributes = attributes if attributes
181 copy
181 copy
182 end
182 end
183
183
184 # Returns true if the issue is a copy
184 # Returns true if the issue is a copy
185 def copy?
185 def copy?
186 @copied_from.present?
186 @copied_from.present?
187 end
187 end
188
188
189 # Moves/copies an issue to a new project and tracker
189 # Moves/copies an issue to a new project and tracker
190 # Returns the moved/copied issue on success, false on failure
190 # Returns the moved/copied issue on success, false on failure
191 def move_to_project(new_project, new_tracker=nil, options={})
191 def move_to_project(new_project, new_tracker=nil, options={})
192 ActiveSupport::Deprecation.warn "Issue#move_to_project is deprecated, use #project= instead."
192 ActiveSupport::Deprecation.warn "Issue#move_to_project is deprecated, use #project= instead."
193
193
194 if options[:copy]
194 if options[:copy]
195 issue = self.copy
195 issue = self.copy
196 else
196 else
197 issue = self
197 issue = self
198 end
198 end
199
199
200 issue.init_journal(User.current, options[:notes])
200 issue.init_journal(User.current, options[:notes])
201
201
202 # Preserve previous behaviour
202 # Preserve previous behaviour
203 # #move_to_project doesn't change tracker automatically
203 # #move_to_project doesn't change tracker automatically
204 issue.send :project=, new_project, true
204 issue.send :project=, new_project, true
205 if new_tracker
205 if new_tracker
206 issue.tracker = new_tracker
206 issue.tracker = new_tracker
207 end
207 end
208 # Allow bulk setting of attributes on the issue
208 # Allow bulk setting of attributes on the issue
209 if options[:attributes]
209 if options[:attributes]
210 issue.attributes = options[:attributes]
210 issue.attributes = options[:attributes]
211 end
211 end
212
212
213 issue.save ? issue : false
213 issue.save ? issue : false
214 end
214 end
215
215
216 def status_id=(sid)
216 def status_id=(sid)
217 self.status = nil
217 self.status = nil
218 result = write_attribute(:status_id, sid)
218 result = write_attribute(:status_id, sid)
219 @workflow_rule_by_attribute = nil
219 @workflow_rule_by_attribute = nil
220 result
220 result
221 end
221 end
222
222
223 def priority_id=(pid)
223 def priority_id=(pid)
224 self.priority = nil
224 self.priority = nil
225 write_attribute(:priority_id, pid)
225 write_attribute(:priority_id, pid)
226 end
226 end
227
227
228 def category_id=(cid)
228 def category_id=(cid)
229 self.category = nil
229 self.category = nil
230 write_attribute(:category_id, cid)
230 write_attribute(:category_id, cid)
231 end
231 end
232
232
233 def fixed_version_id=(vid)
233 def fixed_version_id=(vid)
234 self.fixed_version = nil
234 self.fixed_version = nil
235 write_attribute(:fixed_version_id, vid)
235 write_attribute(:fixed_version_id, vid)
236 end
236 end
237
237
238 def tracker_id=(tid)
238 def tracker_id=(tid)
239 self.tracker = nil
239 self.tracker = nil
240 result = write_attribute(:tracker_id, tid)
240 result = write_attribute(:tracker_id, tid)
241 @custom_field_values = nil
241 @custom_field_values = nil
242 @workflow_rule_by_attribute = nil
242 @workflow_rule_by_attribute = nil
243 result
243 result
244 end
244 end
245
245
246 def project_id=(project_id)
246 def project_id=(project_id)
247 if project_id.to_s != self.project_id.to_s
247 if project_id.to_s != self.project_id.to_s
248 self.project = (project_id.present? ? Project.find_by_id(project_id) : nil)
248 self.project = (project_id.present? ? Project.find_by_id(project_id) : nil)
249 end
249 end
250 end
250 end
251
251
252 def project=(project, keep_tracker=false)
252 def project=(project, keep_tracker=false)
253 project_was = self.project
253 project_was = self.project
254 write_attribute(:project_id, project ? project.id : nil)
254 write_attribute(:project_id, project ? project.id : nil)
255 association_instance_set('project', project)
255 association_instance_set('project', project)
256 if project_was && project && project_was != project
256 if project_was && project && project_was != project
257 @assignable_versions = nil
257 @assignable_versions = nil
258
258
259 unless keep_tracker || project.trackers.include?(tracker)
259 unless keep_tracker || project.trackers.include?(tracker)
260 self.tracker = project.trackers.first
260 self.tracker = project.trackers.first
261 end
261 end
262 # Reassign to the category with same name if any
262 # Reassign to the category with same name if any
263 if category
263 if category
264 self.category = project.issue_categories.find_by_name(category.name)
264 self.category = project.issue_categories.find_by_name(category.name)
265 end
265 end
266 # Keep the fixed_version if it's still valid in the new_project
266 # Keep the fixed_version if it's still valid in the new_project
267 if fixed_version && fixed_version.project != project && !project.shared_versions.include?(fixed_version)
267 if fixed_version && fixed_version.project != project && !project.shared_versions.include?(fixed_version)
268 self.fixed_version = nil
268 self.fixed_version = nil
269 end
269 end
270 if parent && parent.project_id != project_id
270 if parent && parent.project_id != project_id
271 self.parent_issue_id = nil
271 self.parent_issue_id = nil
272 end
272 end
273 @custom_field_values = nil
273 @custom_field_values = nil
274 end
274 end
275 end
275 end
276
276
277 def description=(arg)
277 def description=(arg)
278 if arg.is_a?(String)
278 if arg.is_a?(String)
279 arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n")
279 arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n")
280 end
280 end
281 write_attribute(:description, arg)
281 write_attribute(:description, arg)
282 end
282 end
283
283
284 # Overrides assign_attributes so that project and tracker get assigned first
284 # Overrides assign_attributes so that project and tracker get assigned first
285 def assign_attributes_with_project_and_tracker_first(new_attributes, *args)
285 def assign_attributes_with_project_and_tracker_first(new_attributes, *args)
286 return if new_attributes.nil?
286 return if new_attributes.nil?
287 attrs = new_attributes.dup
287 attrs = new_attributes.dup
288 attrs.stringify_keys!
288 attrs.stringify_keys!
289
289
290 %w(project project_id tracker tracker_id).each do |attr|
290 %w(project project_id tracker tracker_id).each do |attr|
291 if attrs.has_key?(attr)
291 if attrs.has_key?(attr)
292 send "#{attr}=", attrs.delete(attr)
292 send "#{attr}=", attrs.delete(attr)
293 end
293 end
294 end
294 end
295 send :assign_attributes_without_project_and_tracker_first, attrs, *args
295 send :assign_attributes_without_project_and_tracker_first, attrs, *args
296 end
296 end
297 # Do not redefine alias chain on reload (see #4838)
297 # Do not redefine alias chain on reload (see #4838)
298 alias_method_chain(:assign_attributes, :project_and_tracker_first) unless method_defined?(:assign_attributes_without_project_and_tracker_first)
298 alias_method_chain(:assign_attributes, :project_and_tracker_first) unless method_defined?(:assign_attributes_without_project_and_tracker_first)
299
299
300 def estimated_hours=(h)
300 def estimated_hours=(h)
301 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
301 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
302 end
302 end
303
303
304 safe_attributes 'project_id',
304 safe_attributes 'project_id',
305 :if => lambda {|issue, user|
305 :if => lambda {|issue, user|
306 if issue.new_record?
306 if issue.new_record?
307 issue.copy?
307 issue.copy?
308 elsif user.allowed_to?(:move_issues, issue.project)
308 elsif user.allowed_to?(:move_issues, issue.project)
309 projects = Issue.allowed_target_projects_on_move(user)
309 projects = Issue.allowed_target_projects_on_move(user)
310 projects.include?(issue.project) && projects.size > 1
310 projects.include?(issue.project) && projects.size > 1
311 end
311 end
312 }
312 }
313
313
314 safe_attributes 'tracker_id',
314 safe_attributes 'tracker_id',
315 'status_id',
315 'status_id',
316 'category_id',
316 'category_id',
317 'assigned_to_id',
317 'assigned_to_id',
318 'priority_id',
318 'priority_id',
319 'fixed_version_id',
319 'fixed_version_id',
320 'subject',
320 'subject',
321 'description',
321 'description',
322 'start_date',
322 'start_date',
323 'due_date',
323 'due_date',
324 'done_ratio',
324 'done_ratio',
325 'estimated_hours',
325 'estimated_hours',
326 'custom_field_values',
326 'custom_field_values',
327 'custom_fields',
327 'custom_fields',
328 'lock_version',
328 'lock_version',
329 :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
329 :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
330
330
331 safe_attributes 'status_id',
331 safe_attributes 'status_id',
332 'assigned_to_id',
332 'assigned_to_id',
333 'fixed_version_id',
333 'fixed_version_id',
334 'done_ratio',
334 'done_ratio',
335 'lock_version',
335 'lock_version',
336 :if => lambda {|issue, user| issue.new_statuses_allowed_to(user).any? }
336 :if => lambda {|issue, user| issue.new_statuses_allowed_to(user).any? }
337
337
338 safe_attributes 'watcher_user_ids',
338 safe_attributes 'watcher_user_ids',
339 :if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)}
339 :if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)}
340
340
341 safe_attributes 'is_private',
341 safe_attributes 'is_private',
342 :if => lambda {|issue, user|
342 :if => lambda {|issue, user|
343 user.allowed_to?(:set_issues_private, issue.project) ||
343 user.allowed_to?(:set_issues_private, issue.project) ||
344 (issue.author == user && user.allowed_to?(:set_own_issues_private, issue.project))
344 (issue.author == user && user.allowed_to?(:set_own_issues_private, issue.project))
345 }
345 }
346
346
347 safe_attributes 'parent_issue_id',
347 safe_attributes 'parent_issue_id',
348 :if => lambda {|issue, user| (issue.new_record? || user.allowed_to?(:edit_issues, issue.project)) &&
348 :if => lambda {|issue, user| (issue.new_record? || user.allowed_to?(:edit_issues, issue.project)) &&
349 user.allowed_to?(:manage_subtasks, issue.project)}
349 user.allowed_to?(:manage_subtasks, issue.project)}
350
350
351 def safe_attribute_names(user=nil)
351 def safe_attribute_names(user=nil)
352 names = super
352 names = super
353 names -= disabled_core_fields
353 names -= disabled_core_fields
354 names -= read_only_attribute_names(user)
354 names -= read_only_attribute_names(user)
355 names
355 names
356 end
356 end
357
357
358 # Safely sets attributes
358 # Safely sets attributes
359 # Should be called from controllers instead of #attributes=
359 # Should be called from controllers instead of #attributes=
360 # attr_accessible is too rough because we still want things like
360 # attr_accessible is too rough because we still want things like
361 # Issue.new(:project => foo) to work
361 # Issue.new(:project => foo) to work
362 def safe_attributes=(attrs, user=User.current)
362 def safe_attributes=(attrs, user=User.current)
363 return unless attrs.is_a?(Hash)
363 return unless attrs.is_a?(Hash)
364
364
365 attrs = attrs.dup
365 attrs = attrs.dup
366
366
367 # Project and Tracker must be set before since new_statuses_allowed_to depends on it.
367 # Project and Tracker must be set before since new_statuses_allowed_to depends on it.
368 if (p = attrs.delete('project_id')) && safe_attribute?('project_id')
368 if (p = attrs.delete('project_id')) && safe_attribute?('project_id')
369 if allowed_target_projects(user).collect(&:id).include?(p.to_i)
369 if allowed_target_projects(user).collect(&:id).include?(p.to_i)
370 self.project_id = p
370 self.project_id = p
371 end
371 end
372 end
372 end
373
373
374 if (t = attrs.delete('tracker_id')) && safe_attribute?('tracker_id')
374 if (t = attrs.delete('tracker_id')) && safe_attribute?('tracker_id')
375 self.tracker_id = t
375 self.tracker_id = t
376 end
376 end
377
377
378 if (s = attrs.delete('status_id')) && safe_attribute?('status_id')
378 if (s = attrs.delete('status_id')) && safe_attribute?('status_id')
379 if new_statuses_allowed_to(user).collect(&:id).include?(s.to_i)
379 if new_statuses_allowed_to(user).collect(&:id).include?(s.to_i)
380 self.status_id = s
380 self.status_id = s
381 end
381 end
382 end
382 end
383
383
384 attrs = delete_unsafe_attributes(attrs, user)
384 attrs = delete_unsafe_attributes(attrs, user)
385 return if attrs.empty?
385 return if attrs.empty?
386
386
387 unless leaf?
387 unless leaf?
388 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
388 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
389 end
389 end
390
390
391 if attrs['parent_issue_id'].present?
391 if attrs['parent_issue_id'].present?
392 attrs.delete('parent_issue_id') unless Issue.visible(user).exists?(attrs['parent_issue_id'].to_i)
392 attrs.delete('parent_issue_id') unless Issue.visible(user).exists?(attrs['parent_issue_id'].to_i)
393 end
393 end
394
394
395 if attrs['custom_field_values'].present?
395 if attrs['custom_field_values'].present?
396 attrs['custom_field_values'] = attrs['custom_field_values'].reject {|k, v| read_only_attribute_names(user).include? k.to_s}
396 attrs['custom_field_values'] = attrs['custom_field_values'].reject {|k, v| read_only_attribute_names(user).include? k.to_s}
397 end
397 end
398
398
399 if attrs['custom_fields'].present?
399 if attrs['custom_fields'].present?
400 attrs['custom_fields'] = attrs['custom_fields'].reject {|c| read_only_attribute_names(user).include? c['id'].to_s}
400 attrs['custom_fields'] = attrs['custom_fields'].reject {|c| read_only_attribute_names(user).include? c['id'].to_s}
401 end
401 end
402
402
403 # mass-assignment security bypass
403 # mass-assignment security bypass
404 assign_attributes attrs, :without_protection => true
404 assign_attributes attrs, :without_protection => true
405 end
405 end
406
406
407 def disabled_core_fields
407 def disabled_core_fields
408 tracker ? tracker.disabled_core_fields : []
408 tracker ? tracker.disabled_core_fields : []
409 end
409 end
410
410
411 # Returns the custom_field_values that can be edited by the given user
411 # Returns the custom_field_values that can be edited by the given user
412 def editable_custom_field_values(user=nil)
412 def editable_custom_field_values(user=nil)
413 custom_field_values.reject do |value|
413 custom_field_values.reject do |value|
414 read_only_attribute_names(user).include?(value.custom_field_id.to_s)
414 read_only_attribute_names(user).include?(value.custom_field_id.to_s)
415 end
415 end
416 end
416 end
417
417
418 # Returns the names of attributes that are read-only for user or the current user
418 # Returns the names of attributes that are read-only for user or the current user
419 # For users with multiple roles, the read-only fields are the intersection of
419 # For users with multiple roles, the read-only fields are the intersection of
420 # read-only fields of each role
420 # read-only fields of each role
421 # The result is an array of strings where sustom fields are represented with their ids
421 # The result is an array of strings where sustom fields are represented with their ids
422 #
422 #
423 # Examples:
423 # Examples:
424 # issue.read_only_attribute_names # => ['due_date', '2']
424 # issue.read_only_attribute_names # => ['due_date', '2']
425 # issue.read_only_attribute_names(user) # => []
425 # issue.read_only_attribute_names(user) # => []
426 def read_only_attribute_names(user=nil)
426 def read_only_attribute_names(user=nil)
427 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'readonly'}.keys
427 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'readonly'}.keys
428 end
428 end
429
429
430 # Returns the names of required attributes for user or the current user
430 # Returns the names of required attributes for user or the current user
431 # For users with multiple roles, the required fields are the intersection of
431 # For users with multiple roles, the required fields are the intersection of
432 # required fields of each role
432 # required fields of each role
433 # The result is an array of strings where sustom fields are represented with their ids
433 # The result is an array of strings where sustom fields are represented with their ids
434 #
434 #
435 # Examples:
435 # Examples:
436 # issue.required_attribute_names # => ['due_date', '2']
436 # issue.required_attribute_names # => ['due_date', '2']
437 # issue.required_attribute_names(user) # => []
437 # issue.required_attribute_names(user) # => []
438 def required_attribute_names(user=nil)
438 def required_attribute_names(user=nil)
439 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'required'}.keys
439 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'required'}.keys
440 end
440 end
441
441
442 # Returns true if the attribute is required for user
442 # Returns true if the attribute is required for user
443 def required_attribute?(name, user=nil)
443 def required_attribute?(name, user=nil)
444 required_attribute_names(user).include?(name.to_s)
444 required_attribute_names(user).include?(name.to_s)
445 end
445 end
446
446
447 # Returns a hash of the workflow rule by attribute for the given user
447 # Returns a hash of the workflow rule by attribute for the given user
448 #
448 #
449 # Examples:
449 # Examples:
450 # issue.workflow_rule_by_attribute # => {'due_date' => 'required', 'start_date' => 'readonly'}
450 # issue.workflow_rule_by_attribute # => {'due_date' => 'required', 'start_date' => 'readonly'}
451 def workflow_rule_by_attribute(user=nil)
451 def workflow_rule_by_attribute(user=nil)
452 return @workflow_rule_by_attribute if @workflow_rule_by_attribute && user.nil?
452 return @workflow_rule_by_attribute if @workflow_rule_by_attribute && user.nil?
453
453
454 user_real = user || User.current
454 user_real = user || User.current
455 roles = user_real.admin ? Role.all : user_real.roles_for_project(project)
455 roles = user_real.admin ? Role.all : user_real.roles_for_project(project)
456 return {} if roles.empty?
456 return {} if roles.empty?
457
457
458 result = {}
458 result = {}
459 workflow_permissions = WorkflowPermission.where(:tracker_id => tracker_id, :old_status_id => status_id, :role_id => roles.map(&:id)).all
459 workflow_permissions = WorkflowPermission.where(:tracker_id => tracker_id, :old_status_id => status_id, :role_id => roles.map(&:id)).all
460 if workflow_permissions.any?
460 if workflow_permissions.any?
461 workflow_rules = workflow_permissions.inject({}) do |h, wp|
461 workflow_rules = workflow_permissions.inject({}) do |h, wp|
462 h[wp.field_name] ||= []
462 h[wp.field_name] ||= []
463 h[wp.field_name] << wp.rule
463 h[wp.field_name] << wp.rule
464 h
464 h
465 end
465 end
466 workflow_rules.each do |attr, rules|
466 workflow_rules.each do |attr, rules|
467 next if rules.size < roles.size
467 next if rules.size < roles.size
468 uniq_rules = rules.uniq
468 uniq_rules = rules.uniq
469 if uniq_rules.size == 1
469 if uniq_rules.size == 1
470 result[attr] = uniq_rules.first
470 result[attr] = uniq_rules.first
471 else
471 else
472 result[attr] = 'required'
472 result[attr] = 'required'
473 end
473 end
474 end
474 end
475 end
475 end
476 @workflow_rule_by_attribute = result if user.nil?
476 @workflow_rule_by_attribute = result if user.nil?
477 result
477 result
478 end
478 end
479 private :workflow_rule_by_attribute
479 private :workflow_rule_by_attribute
480
480
481 def done_ratio
481 def done_ratio
482 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
482 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
483 status.default_done_ratio
483 status.default_done_ratio
484 else
484 else
485 read_attribute(:done_ratio)
485 read_attribute(:done_ratio)
486 end
486 end
487 end
487 end
488
488
489 def self.use_status_for_done_ratio?
489 def self.use_status_for_done_ratio?
490 Setting.issue_done_ratio == 'issue_status'
490 Setting.issue_done_ratio == 'issue_status'
491 end
491 end
492
492
493 def self.use_field_for_done_ratio?
493 def self.use_field_for_done_ratio?
494 Setting.issue_done_ratio == 'issue_field'
494 Setting.issue_done_ratio == 'issue_field'
495 end
495 end
496
496
497 def validate_issue
497 def validate_issue
498 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
498 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
499 errors.add :due_date, :not_a_date
499 errors.add :due_date, :not_a_date
500 end
500 end
501
501
502 if self.due_date and self.start_date and self.due_date < self.start_date
502 if self.due_date and self.start_date and self.due_date < self.start_date
503 errors.add :due_date, :greater_than_start_date
503 errors.add :due_date, :greater_than_start_date
504 end
504 end
505
505
506 if start_date && soonest_start && start_date < soonest_start
506 if start_date && soonest_start && start_date < soonest_start
507 errors.add :start_date, :invalid
507 errors.add :start_date, :invalid
508 end
508 end
509
509
510 if fixed_version
510 if fixed_version
511 if !assignable_versions.include?(fixed_version)
511 if !assignable_versions.include?(fixed_version)
512 errors.add :fixed_version_id, :inclusion
512 errors.add :fixed_version_id, :inclusion
513 elsif reopened? && fixed_version.closed?
513 elsif reopened? && fixed_version.closed?
514 errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version)
514 errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version)
515 end
515 end
516 end
516 end
517
517
518 # Checks that the issue can not be added/moved to a disabled tracker
518 # Checks that the issue can not be added/moved to a disabled tracker
519 if project && (tracker_id_changed? || project_id_changed?)
519 if project && (tracker_id_changed? || project_id_changed?)
520 unless project.trackers.include?(tracker)
520 unless project.trackers.include?(tracker)
521 errors.add :tracker_id, :inclusion
521 errors.add :tracker_id, :inclusion
522 end
522 end
523 end
523 end
524
524
525 # Checks parent issue assignment
525 # Checks parent issue assignment
526 if @parent_issue
526 if @parent_issue
527 if @parent_issue.project_id != project_id
527 if @parent_issue.project_id != project_id
528 errors.add :parent_issue_id, :not_same_project
528 errors.add :parent_issue_id, :not_same_project
529 elsif !new_record?
529 elsif !new_record?
530 # moving an existing issue
530 # moving an existing issue
531 if @parent_issue.root_id != root_id
531 if @parent_issue.root_id != root_id
532 # we can always move to another tree
532 # we can always move to another tree
533 elsif move_possible?(@parent_issue)
533 elsif move_possible?(@parent_issue)
534 # move accepted inside tree
534 # move accepted inside tree
535 else
535 else
536 errors.add :parent_issue_id, :not_a_valid_parent
536 errors.add :parent_issue_id, :not_a_valid_parent
537 end
537 end
538 end
538 end
539 end
539 end
540 end
540 end
541
541
542 # Validates the issue against additional workflow requirements
542 # Validates the issue against additional workflow requirements
543 def validate_required_fields
543 def validate_required_fields
544 user = new_record? ? author : current_journal.try(:user)
544 user = new_record? ? author : current_journal.try(:user)
545
545
546 required_attribute_names(user).each do |attribute|
546 required_attribute_names(user).each do |attribute|
547 if attribute =~ /^\d+$/
547 if attribute =~ /^\d+$/
548 attribute = attribute.to_i
548 attribute = attribute.to_i
549 v = custom_field_values.detect {|v| v.custom_field_id == attribute }
549 v = custom_field_values.detect {|v| v.custom_field_id == attribute }
550 if v && v.value.blank?
550 if v && v.value.blank?
551 errors.add :base, v.custom_field.name + ' ' + l('activerecord.errors.messages.blank')
551 errors.add :base, v.custom_field.name + ' ' + l('activerecord.errors.messages.blank')
552 end
552 end
553 else
553 else
554 if respond_to?(attribute) && send(attribute).blank?
554 if respond_to?(attribute) && send(attribute).blank?
555 errors.add attribute, :blank
555 errors.add attribute, :blank
556 end
556 end
557 end
557 end
558 end
558 end
559 end
559 end
560
560
561 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
561 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
562 # even if the user turns off the setting later
562 # even if the user turns off the setting later
563 def update_done_ratio_from_issue_status
563 def update_done_ratio_from_issue_status
564 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
564 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
565 self.done_ratio = status.default_done_ratio
565 self.done_ratio = status.default_done_ratio
566 end
566 end
567 end
567 end
568
568
569 def init_journal(user, notes = "")
569 def init_journal(user, notes = "")
570 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
570 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
571 if new_record?
571 if new_record?
572 @current_journal.notify = false
572 @current_journal.notify = false
573 else
573 else
574 @attributes_before_change = attributes.dup
574 @attributes_before_change = attributes.dup
575 @custom_values_before_change = {}
575 @custom_values_before_change = {}
576 self.custom_field_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
576 self.custom_field_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
577 end
577 end
578 @current_journal
578 @current_journal
579 end
579 end
580
580
581 # Returns the id of the last journal or nil
581 # Returns the id of the last journal or nil
582 def last_journal_id
582 def last_journal_id
583 if new_record?
583 if new_record?
584 nil
584 nil
585 else
585 else
586 journals.maximum(:id)
586 journals.maximum(:id)
587 end
587 end
588 end
588 end
589
589
590 # Returns a scope for journals that have an id greater than journal_id
591 def journals_after(journal_id)
592 scope = journals.reorder("#{Journal.table_name}.id ASC")
593 if journal_id.present?
594 scope = scope.where("#{Journal.table_name}.id > ?", journal_id.to_i)
595 end
596 scope
597 end
598
590 # Return true if the issue is closed, otherwise false
599 # Return true if the issue is closed, otherwise false
591 def closed?
600 def closed?
592 self.status.is_closed?
601 self.status.is_closed?
593 end
602 end
594
603
595 # Return true if the issue is being reopened
604 # Return true if the issue is being reopened
596 def reopened?
605 def reopened?
597 if !new_record? && status_id_changed?
606 if !new_record? && status_id_changed?
598 status_was = IssueStatus.find_by_id(status_id_was)
607 status_was = IssueStatus.find_by_id(status_id_was)
599 status_new = IssueStatus.find_by_id(status_id)
608 status_new = IssueStatus.find_by_id(status_id)
600 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
609 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
601 return true
610 return true
602 end
611 end
603 end
612 end
604 false
613 false
605 end
614 end
606
615
607 # Return true if the issue is being closed
616 # Return true if the issue is being closed
608 def closing?
617 def closing?
609 if !new_record? && status_id_changed?
618 if !new_record? && status_id_changed?
610 status_was = IssueStatus.find_by_id(status_id_was)
619 status_was = IssueStatus.find_by_id(status_id_was)
611 status_new = IssueStatus.find_by_id(status_id)
620 status_new = IssueStatus.find_by_id(status_id)
612 if status_was && status_new && !status_was.is_closed? && status_new.is_closed?
621 if status_was && status_new && !status_was.is_closed? && status_new.is_closed?
613 return true
622 return true
614 end
623 end
615 end
624 end
616 false
625 false
617 end
626 end
618
627
619 # Returns true if the issue is overdue
628 # Returns true if the issue is overdue
620 def overdue?
629 def overdue?
621 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
630 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
622 end
631 end
623
632
624 # Is the amount of work done less than it should for the due date
633 # Is the amount of work done less than it should for the due date
625 def behind_schedule?
634 def behind_schedule?
626 return false if start_date.nil? || due_date.nil?
635 return false if start_date.nil? || due_date.nil?
627 done_date = start_date + ((due_date - start_date+1)* done_ratio/100).floor
636 done_date = start_date + ((due_date - start_date+1)* done_ratio/100).floor
628 return done_date <= Date.today
637 return done_date <= Date.today
629 end
638 end
630
639
631 # Does this issue have children?
640 # Does this issue have children?
632 def children?
641 def children?
633 !leaf?
642 !leaf?
634 end
643 end
635
644
636 # Users the issue can be assigned to
645 # Users the issue can be assigned to
637 def assignable_users
646 def assignable_users
638 users = project.assignable_users
647 users = project.assignable_users
639 users << author if author
648 users << author if author
640 users << assigned_to if assigned_to
649 users << assigned_to if assigned_to
641 users.uniq.sort
650 users.uniq.sort
642 end
651 end
643
652
644 # Versions that the issue can be assigned to
653 # Versions that the issue can be assigned to
645 def assignable_versions
654 def assignable_versions
646 return @assignable_versions if @assignable_versions
655 return @assignable_versions if @assignable_versions
647
656
648 versions = project.shared_versions.open.all
657 versions = project.shared_versions.open.all
649 if fixed_version
658 if fixed_version
650 if fixed_version_id_changed?
659 if fixed_version_id_changed?
651 # nothing to do
660 # nothing to do
652 elsif project_id_changed?
661 elsif project_id_changed?
653 if project.shared_versions.include?(fixed_version)
662 if project.shared_versions.include?(fixed_version)
654 versions << fixed_version
663 versions << fixed_version
655 end
664 end
656 else
665 else
657 versions << fixed_version
666 versions << fixed_version
658 end
667 end
659 end
668 end
660 @assignable_versions = versions.uniq.sort
669 @assignable_versions = versions.uniq.sort
661 end
670 end
662
671
663 # Returns true if this issue is blocked by another issue that is still open
672 # Returns true if this issue is blocked by another issue that is still open
664 def blocked?
673 def blocked?
665 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
674 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
666 end
675 end
667
676
668 # Returns an array of statuses that user is able to apply
677 # Returns an array of statuses that user is able to apply
669 def new_statuses_allowed_to(user=User.current, include_default=false)
678 def new_statuses_allowed_to(user=User.current, include_default=false)
670 if new_record? && @copied_from
679 if new_record? && @copied_from
671 [IssueStatus.default, @copied_from.status].compact.uniq.sort
680 [IssueStatus.default, @copied_from.status].compact.uniq.sort
672 else
681 else
673 initial_status = nil
682 initial_status = nil
674 if new_record?
683 if new_record?
675 initial_status = IssueStatus.default
684 initial_status = IssueStatus.default
676 elsif status_id_was
685 elsif status_id_was
677 initial_status = IssueStatus.find_by_id(status_id_was)
686 initial_status = IssueStatus.find_by_id(status_id_was)
678 end
687 end
679 initial_status ||= status
688 initial_status ||= status
680
689
681 statuses = initial_status.find_new_statuses_allowed_to(
690 statuses = initial_status.find_new_statuses_allowed_to(
682 user.admin ? Role.all : user.roles_for_project(project),
691 user.admin ? Role.all : user.roles_for_project(project),
683 tracker,
692 tracker,
684 author == user,
693 author == user,
685 assigned_to_id_changed? ? assigned_to_id_was == user.id : assigned_to_id == user.id
694 assigned_to_id_changed? ? assigned_to_id_was == user.id : assigned_to_id == user.id
686 )
695 )
687 statuses << initial_status unless statuses.empty?
696 statuses << initial_status unless statuses.empty?
688 statuses << IssueStatus.default if include_default
697 statuses << IssueStatus.default if include_default
689 statuses = statuses.compact.uniq.sort
698 statuses = statuses.compact.uniq.sort
690 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
699 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
691 end
700 end
692 end
701 end
693
702
694 def assigned_to_was
703 def assigned_to_was
695 if assigned_to_id_changed? && assigned_to_id_was.present?
704 if assigned_to_id_changed? && assigned_to_id_was.present?
696 @assigned_to_was ||= User.find_by_id(assigned_to_id_was)
705 @assigned_to_was ||= User.find_by_id(assigned_to_id_was)
697 end
706 end
698 end
707 end
699
708
700 # Returns the mail adresses of users that should be notified
709 # Returns the mail adresses of users that should be notified
701 def recipients
710 def recipients
702 notified = []
711 notified = []
703 # Author and assignee are always notified unless they have been
712 # Author and assignee are always notified unless they have been
704 # locked or don't want to be notified
713 # locked or don't want to be notified
705 notified << author if author
714 notified << author if author
706 if assigned_to
715 if assigned_to
707 notified += (assigned_to.is_a?(Group) ? assigned_to.users : [assigned_to])
716 notified += (assigned_to.is_a?(Group) ? assigned_to.users : [assigned_to])
708 end
717 end
709 if assigned_to_was
718 if assigned_to_was
710 notified += (assigned_to_was.is_a?(Group) ? assigned_to_was.users : [assigned_to_was])
719 notified += (assigned_to_was.is_a?(Group) ? assigned_to_was.users : [assigned_to_was])
711 end
720 end
712 notified = notified.select {|u| u.active? && u.notify_about?(self)}
721 notified = notified.select {|u| u.active? && u.notify_about?(self)}
713
722
714 notified += project.notified_users
723 notified += project.notified_users
715 notified.uniq!
724 notified.uniq!
716 # Remove users that can not view the issue
725 # Remove users that can not view the issue
717 notified.reject! {|user| !visible?(user)}
726 notified.reject! {|user| !visible?(user)}
718 notified.collect(&:mail)
727 notified.collect(&:mail)
719 end
728 end
720
729
721 # Returns the number of hours spent on this issue
730 # Returns the number of hours spent on this issue
722 def spent_hours
731 def spent_hours
723 @spent_hours ||= time_entries.sum(:hours) || 0
732 @spent_hours ||= time_entries.sum(:hours) || 0
724 end
733 end
725
734
726 # Returns the total number of hours spent on this issue and its descendants
735 # Returns the total number of hours spent on this issue and its descendants
727 #
736 #
728 # Example:
737 # Example:
729 # spent_hours => 0.0
738 # spent_hours => 0.0
730 # spent_hours => 50.2
739 # spent_hours => 50.2
731 def total_spent_hours
740 def total_spent_hours
732 @total_spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours",
741 @total_spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours",
733 :joins => "LEFT JOIN #{TimeEntry.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id").to_f || 0.0
742 :joins => "LEFT JOIN #{TimeEntry.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id").to_f || 0.0
734 end
743 end
735
744
736 def relations
745 def relations
737 @relations ||= (relations_from + relations_to).sort
746 @relations ||= (relations_from + relations_to).sort
738 end
747 end
739
748
740 # Preloads relations for a collection of issues
749 # Preloads relations for a collection of issues
741 def self.load_relations(issues)
750 def self.load_relations(issues)
742 if issues.any?
751 if issues.any?
743 relations = IssueRelation.all(:conditions => ["issue_from_id IN (:ids) OR issue_to_id IN (:ids)", {:ids => issues.map(&:id)}])
752 relations = IssueRelation.all(:conditions => ["issue_from_id IN (:ids) OR issue_to_id IN (:ids)", {:ids => issues.map(&:id)}])
744 issues.each do |issue|
753 issues.each do |issue|
745 issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
754 issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
746 end
755 end
747 end
756 end
748 end
757 end
749
758
750 # Preloads visible spent time for a collection of issues
759 # Preloads visible spent time for a collection of issues
751 def self.load_visible_spent_hours(issues, user=User.current)
760 def self.load_visible_spent_hours(issues, user=User.current)
752 if issues.any?
761 if issues.any?
753 hours_by_issue_id = TimeEntry.visible(user).sum(:hours, :group => :issue_id)
762 hours_by_issue_id = TimeEntry.visible(user).sum(:hours, :group => :issue_id)
754 issues.each do |issue|
763 issues.each do |issue|
755 issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0)
764 issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0)
756 end
765 end
757 end
766 end
758 end
767 end
759
768
760 # Finds an issue relation given its id.
769 # Finds an issue relation given its id.
761 def find_relation(relation_id)
770 def find_relation(relation_id)
762 IssueRelation.find(relation_id, :conditions => ["issue_to_id = ? OR issue_from_id = ?", id, id])
771 IssueRelation.find(relation_id, :conditions => ["issue_to_id = ? OR issue_from_id = ?", id, id])
763 end
772 end
764
773
765 def all_dependent_issues(except=[])
774 def all_dependent_issues(except=[])
766 except << self
775 except << self
767 dependencies = []
776 dependencies = []
768 relations_from.each do |relation|
777 relations_from.each do |relation|
769 if relation.issue_to && !except.include?(relation.issue_to)
778 if relation.issue_to && !except.include?(relation.issue_to)
770 dependencies << relation.issue_to
779 dependencies << relation.issue_to
771 dependencies += relation.issue_to.all_dependent_issues(except)
780 dependencies += relation.issue_to.all_dependent_issues(except)
772 end
781 end
773 end
782 end
774 dependencies
783 dependencies
775 end
784 end
776
785
777 # Returns an array of issues that duplicate this one
786 # Returns an array of issues that duplicate this one
778 def duplicates
787 def duplicates
779 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
788 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
780 end
789 end
781
790
782 # Returns the due date or the target due date if any
791 # Returns the due date or the target due date if any
783 # Used on gantt chart
792 # Used on gantt chart
784 def due_before
793 def due_before
785 due_date || (fixed_version ? fixed_version.effective_date : nil)
794 due_date || (fixed_version ? fixed_version.effective_date : nil)
786 end
795 end
787
796
788 # Returns the time scheduled for this issue.
797 # Returns the time scheduled for this issue.
789 #
798 #
790 # Example:
799 # Example:
791 # Start Date: 2/26/09, End Date: 3/04/09
800 # Start Date: 2/26/09, End Date: 3/04/09
792 # duration => 6
801 # duration => 6
793 def duration
802 def duration
794 (start_date && due_date) ? due_date - start_date : 0
803 (start_date && due_date) ? due_date - start_date : 0
795 end
804 end
796
805
797 def soonest_start
806 def soonest_start
798 @soonest_start ||= (
807 @soonest_start ||= (
799 relations_to.collect{|relation| relation.successor_soonest_start} +
808 relations_to.collect{|relation| relation.successor_soonest_start} +
800 ancestors.collect(&:soonest_start)
809 ancestors.collect(&:soonest_start)
801 ).compact.max
810 ).compact.max
802 end
811 end
803
812
804 def reschedule_after(date)
813 def reschedule_after(date)
805 return if date.nil?
814 return if date.nil?
806 if leaf?
815 if leaf?
807 if start_date.nil? || start_date < date
816 if start_date.nil? || start_date < date
808 self.start_date, self.due_date = date, date + duration
817 self.start_date, self.due_date = date, date + duration
809 begin
818 begin
810 save
819 save
811 rescue ActiveRecord::StaleObjectError
820 rescue ActiveRecord::StaleObjectError
812 reload
821 reload
813 self.start_date, self.due_date = date, date + duration
822 self.start_date, self.due_date = date, date + duration
814 save
823 save
815 end
824 end
816 end
825 end
817 else
826 else
818 leaves.each do |leaf|
827 leaves.each do |leaf|
819 leaf.reschedule_after(date)
828 leaf.reschedule_after(date)
820 end
829 end
821 end
830 end
822 end
831 end
823
832
824 def <=>(issue)
833 def <=>(issue)
825 if issue.nil?
834 if issue.nil?
826 -1
835 -1
827 elsif root_id != issue.root_id
836 elsif root_id != issue.root_id
828 (root_id || 0) <=> (issue.root_id || 0)
837 (root_id || 0) <=> (issue.root_id || 0)
829 else
838 else
830 (lft || 0) <=> (issue.lft || 0)
839 (lft || 0) <=> (issue.lft || 0)
831 end
840 end
832 end
841 end
833
842
834 def to_s
843 def to_s
835 "#{tracker} ##{id}: #{subject}"
844 "#{tracker} ##{id}: #{subject}"
836 end
845 end
837
846
838 # Returns a string of css classes that apply to the issue
847 # Returns a string of css classes that apply to the issue
839 def css_classes
848 def css_classes
840 s = "issue status-#{status_id} priority-#{priority_id}"
849 s = "issue status-#{status_id} priority-#{priority_id}"
841 s << ' closed' if closed?
850 s << ' closed' if closed?
842 s << ' overdue' if overdue?
851 s << ' overdue' if overdue?
843 s << ' child' if child?
852 s << ' child' if child?
844 s << ' parent' unless leaf?
853 s << ' parent' unless leaf?
845 s << ' private' if is_private?
854 s << ' private' if is_private?
846 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
855 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
847 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
856 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
848 s
857 s
849 end
858 end
850
859
851 # Saves an issue and a time_entry from the parameters
860 # Saves an issue and a time_entry from the parameters
852 def save_issue_with_child_records(params, existing_time_entry=nil)
861 def save_issue_with_child_records(params, existing_time_entry=nil)
853 Issue.transaction do
862 Issue.transaction do
854 if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, project)
863 if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, project)
855 @time_entry = existing_time_entry || TimeEntry.new
864 @time_entry = existing_time_entry || TimeEntry.new
856 @time_entry.project = project
865 @time_entry.project = project
857 @time_entry.issue = self
866 @time_entry.issue = self
858 @time_entry.user = User.current
867 @time_entry.user = User.current
859 @time_entry.spent_on = User.current.today
868 @time_entry.spent_on = User.current.today
860 @time_entry.attributes = params[:time_entry]
869 @time_entry.attributes = params[:time_entry]
861 self.time_entries << @time_entry
870 self.time_entries << @time_entry
862 end
871 end
863
872
864 # TODO: Rename hook
873 # TODO: Rename hook
865 Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
874 Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
866 if save
875 if save
867 # TODO: Rename hook
876 # TODO: Rename hook
868 Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
877 Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
869 else
878 else
870 raise ActiveRecord::Rollback
879 raise ActiveRecord::Rollback
871 end
880 end
872 end
881 end
873 end
882 end
874
883
875 # Unassigns issues from +version+ if it's no longer shared with issue's project
884 # Unassigns issues from +version+ if it's no longer shared with issue's project
876 def self.update_versions_from_sharing_change(version)
885 def self.update_versions_from_sharing_change(version)
877 # Update issues assigned to the version
886 # Update issues assigned to the version
878 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
887 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
879 end
888 end
880
889
881 # Unassigns issues from versions that are no longer shared
890 # Unassigns issues from versions that are no longer shared
882 # after +project+ was moved
891 # after +project+ was moved
883 def self.update_versions_from_hierarchy_change(project)
892 def self.update_versions_from_hierarchy_change(project)
884 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
893 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
885 # Update issues of the moved projects and issues assigned to a version of a moved project
894 # Update issues of the moved projects and issues assigned to a version of a moved project
886 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
895 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
887 end
896 end
888
897
889 def parent_issue_id=(arg)
898 def parent_issue_id=(arg)
890 parent_issue_id = arg.blank? ? nil : arg.to_i
899 parent_issue_id = arg.blank? ? nil : arg.to_i
891 if parent_issue_id && @parent_issue = Issue.find_by_id(parent_issue_id)
900 if parent_issue_id && @parent_issue = Issue.find_by_id(parent_issue_id)
892 @parent_issue.id
901 @parent_issue.id
893 else
902 else
894 @parent_issue = nil
903 @parent_issue = nil
895 nil
904 nil
896 end
905 end
897 end
906 end
898
907
899 def parent_issue_id
908 def parent_issue_id
900 if instance_variable_defined? :@parent_issue
909 if instance_variable_defined? :@parent_issue
901 @parent_issue.nil? ? nil : @parent_issue.id
910 @parent_issue.nil? ? nil : @parent_issue.id
902 else
911 else
903 parent_id
912 parent_id
904 end
913 end
905 end
914 end
906
915
907 # Extracted from the ReportsController.
916 # Extracted from the ReportsController.
908 def self.by_tracker(project)
917 def self.by_tracker(project)
909 count_and_group_by(:project => project,
918 count_and_group_by(:project => project,
910 :field => 'tracker_id',
919 :field => 'tracker_id',
911 :joins => Tracker.table_name)
920 :joins => Tracker.table_name)
912 end
921 end
913
922
914 def self.by_version(project)
923 def self.by_version(project)
915 count_and_group_by(:project => project,
924 count_and_group_by(:project => project,
916 :field => 'fixed_version_id',
925 :field => 'fixed_version_id',
917 :joins => Version.table_name)
926 :joins => Version.table_name)
918 end
927 end
919
928
920 def self.by_priority(project)
929 def self.by_priority(project)
921 count_and_group_by(:project => project,
930 count_and_group_by(:project => project,
922 :field => 'priority_id',
931 :field => 'priority_id',
923 :joins => IssuePriority.table_name)
932 :joins => IssuePriority.table_name)
924 end
933 end
925
934
926 def self.by_category(project)
935 def self.by_category(project)
927 count_and_group_by(:project => project,
936 count_and_group_by(:project => project,
928 :field => 'category_id',
937 :field => 'category_id',
929 :joins => IssueCategory.table_name)
938 :joins => IssueCategory.table_name)
930 end
939 end
931
940
932 def self.by_assigned_to(project)
941 def self.by_assigned_to(project)
933 count_and_group_by(:project => project,
942 count_and_group_by(:project => project,
934 :field => 'assigned_to_id',
943 :field => 'assigned_to_id',
935 :joins => User.table_name)
944 :joins => User.table_name)
936 end
945 end
937
946
938 def self.by_author(project)
947 def self.by_author(project)
939 count_and_group_by(:project => project,
948 count_and_group_by(:project => project,
940 :field => 'author_id',
949 :field => 'author_id',
941 :joins => User.table_name)
950 :joins => User.table_name)
942 end
951 end
943
952
944 def self.by_subproject(project)
953 def self.by_subproject(project)
945 ActiveRecord::Base.connection.select_all("select s.id as status_id,
954 ActiveRecord::Base.connection.select_all("select s.id as status_id,
946 s.is_closed as closed,
955 s.is_closed as closed,
947 #{Issue.table_name}.project_id as project_id,
956 #{Issue.table_name}.project_id as project_id,
948 count(#{Issue.table_name}.id) as total
957 count(#{Issue.table_name}.id) as total
949 from
958 from
950 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s
959 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s
951 where
960 where
952 #{Issue.table_name}.status_id=s.id
961 #{Issue.table_name}.status_id=s.id
953 and #{Issue.table_name}.project_id = #{Project.table_name}.id
962 and #{Issue.table_name}.project_id = #{Project.table_name}.id
954 and #{visible_condition(User.current, :project => project, :with_subprojects => true)}
963 and #{visible_condition(User.current, :project => project, :with_subprojects => true)}
955 and #{Issue.table_name}.project_id <> #{project.id}
964 and #{Issue.table_name}.project_id <> #{project.id}
956 group by s.id, s.is_closed, #{Issue.table_name}.project_id") if project.descendants.active.any?
965 group by s.id, s.is_closed, #{Issue.table_name}.project_id") if project.descendants.active.any?
957 end
966 end
958 # End ReportsController extraction
967 # End ReportsController extraction
959
968
960 # Returns an array of projects that user can assign the issue to
969 # Returns an array of projects that user can assign the issue to
961 def allowed_target_projects(user=User.current)
970 def allowed_target_projects(user=User.current)
962 if new_record?
971 if new_record?
963 Project.all(:conditions => Project.allowed_to_condition(user, :add_issues))
972 Project.all(:conditions => Project.allowed_to_condition(user, :add_issues))
964 else
973 else
965 self.class.allowed_target_projects_on_move(user)
974 self.class.allowed_target_projects_on_move(user)
966 end
975 end
967 end
976 end
968
977
969 # Returns an array of projects that user can move issues to
978 # Returns an array of projects that user can move issues to
970 def self.allowed_target_projects_on_move(user=User.current)
979 def self.allowed_target_projects_on_move(user=User.current)
971 Project.all(:conditions => Project.allowed_to_condition(user, :move_issues))
980 Project.all(:conditions => Project.allowed_to_condition(user, :move_issues))
972 end
981 end
973
982
974 private
983 private
975
984
976 def after_project_change
985 def after_project_change
977 # Update project_id on related time entries
986 # Update project_id on related time entries
978 TimeEntry.update_all(["project_id = ?", project_id], {:issue_id => id})
987 TimeEntry.update_all(["project_id = ?", project_id], {:issue_id => id})
979
988
980 # Delete issue relations
989 # Delete issue relations
981 unless Setting.cross_project_issue_relations?
990 unless Setting.cross_project_issue_relations?
982 relations_from.clear
991 relations_from.clear
983 relations_to.clear
992 relations_to.clear
984 end
993 end
985
994
986 # Move subtasks
995 # Move subtasks
987 children.each do |child|
996 children.each do |child|
988 # Change project and keep project
997 # Change project and keep project
989 child.send :project=, project, true
998 child.send :project=, project, true
990 unless child.save
999 unless child.save
991 raise ActiveRecord::Rollback
1000 raise ActiveRecord::Rollback
992 end
1001 end
993 end
1002 end
994 end
1003 end
995
1004
996 def update_nested_set_attributes
1005 def update_nested_set_attributes
997 if root_id.nil?
1006 if root_id.nil?
998 # issue was just created
1007 # issue was just created
999 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
1008 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
1000 set_default_left_and_right
1009 set_default_left_and_right
1001 Issue.update_all("root_id = #{root_id}, lft = #{lft}, rgt = #{rgt}", ["id = ?", id])
1010 Issue.update_all("root_id = #{root_id}, lft = #{lft}, rgt = #{rgt}", ["id = ?", id])
1002 if @parent_issue
1011 if @parent_issue
1003 move_to_child_of(@parent_issue)
1012 move_to_child_of(@parent_issue)
1004 end
1013 end
1005 reload
1014 reload
1006 elsif parent_issue_id != parent_id
1015 elsif parent_issue_id != parent_id
1007 former_parent_id = parent_id
1016 former_parent_id = parent_id
1008 # moving an existing issue
1017 # moving an existing issue
1009 if @parent_issue && @parent_issue.root_id == root_id
1018 if @parent_issue && @parent_issue.root_id == root_id
1010 # inside the same tree
1019 # inside the same tree
1011 move_to_child_of(@parent_issue)
1020 move_to_child_of(@parent_issue)
1012 else
1021 else
1013 # to another tree
1022 # to another tree
1014 unless root?
1023 unless root?
1015 move_to_right_of(root)
1024 move_to_right_of(root)
1016 reload
1025 reload
1017 end
1026 end
1018 old_root_id = root_id
1027 old_root_id = root_id
1019 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
1028 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
1020 target_maxright = nested_set_scope.maximum(right_column_name) || 0
1029 target_maxright = nested_set_scope.maximum(right_column_name) || 0
1021 offset = target_maxright + 1 - lft
1030 offset = target_maxright + 1 - lft
1022 Issue.update_all("root_id = #{root_id}, lft = lft + #{offset}, rgt = rgt + #{offset}",
1031 Issue.update_all("root_id = #{root_id}, lft = lft + #{offset}, rgt = rgt + #{offset}",
1023 ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
1032 ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
1024 self[left_column_name] = lft + offset
1033 self[left_column_name] = lft + offset
1025 self[right_column_name] = rgt + offset
1034 self[right_column_name] = rgt + offset
1026 if @parent_issue
1035 if @parent_issue
1027 move_to_child_of(@parent_issue)
1036 move_to_child_of(@parent_issue)
1028 end
1037 end
1029 end
1038 end
1030 reload
1039 reload
1031 # delete invalid relations of all descendants
1040 # delete invalid relations of all descendants
1032 self_and_descendants.each do |issue|
1041 self_and_descendants.each do |issue|
1033 issue.relations.each do |relation|
1042 issue.relations.each do |relation|
1034 relation.destroy unless relation.valid?
1043 relation.destroy unless relation.valid?
1035 end
1044 end
1036 end
1045 end
1037 # update former parent
1046 # update former parent
1038 recalculate_attributes_for(former_parent_id) if former_parent_id
1047 recalculate_attributes_for(former_parent_id) if former_parent_id
1039 end
1048 end
1040 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
1049 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
1041 end
1050 end
1042
1051
1043 def update_parent_attributes
1052 def update_parent_attributes
1044 recalculate_attributes_for(parent_id) if parent_id
1053 recalculate_attributes_for(parent_id) if parent_id
1045 end
1054 end
1046
1055
1047 def recalculate_attributes_for(issue_id)
1056 def recalculate_attributes_for(issue_id)
1048 if issue_id && p = Issue.find_by_id(issue_id)
1057 if issue_id && p = Issue.find_by_id(issue_id)
1049 # priority = highest priority of children
1058 # priority = highest priority of children
1050 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :joins => :priority)
1059 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :joins => :priority)
1051 p.priority = IssuePriority.find_by_position(priority_position)
1060 p.priority = IssuePriority.find_by_position(priority_position)
1052 end
1061 end
1053
1062
1054 # start/due dates = lowest/highest dates of children
1063 # start/due dates = lowest/highest dates of children
1055 p.start_date = p.children.minimum(:start_date)
1064 p.start_date = p.children.minimum(:start_date)
1056 p.due_date = p.children.maximum(:due_date)
1065 p.due_date = p.children.maximum(:due_date)
1057 if p.start_date && p.due_date && p.due_date < p.start_date
1066 if p.start_date && p.due_date && p.due_date < p.start_date
1058 p.start_date, p.due_date = p.due_date, p.start_date
1067 p.start_date, p.due_date = p.due_date, p.start_date
1059 end
1068 end
1060
1069
1061 # done ratio = weighted average ratio of leaves
1070 # done ratio = weighted average ratio of leaves
1062 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
1071 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
1063 leaves_count = p.leaves.count
1072 leaves_count = p.leaves.count
1064 if leaves_count > 0
1073 if leaves_count > 0
1065 average = p.leaves.average(:estimated_hours).to_f
1074 average = p.leaves.average(:estimated_hours).to_f
1066 if average == 0
1075 if average == 0
1067 average = 1
1076 average = 1
1068 end
1077 end
1069 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
1078 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
1070 progress = done / (average * leaves_count)
1079 progress = done / (average * leaves_count)
1071 p.done_ratio = progress.round
1080 p.done_ratio = progress.round
1072 end
1081 end
1073 end
1082 end
1074
1083
1075 # estimate = sum of leaves estimates
1084 # estimate = sum of leaves estimates
1076 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
1085 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
1077 p.estimated_hours = nil if p.estimated_hours == 0.0
1086 p.estimated_hours = nil if p.estimated_hours == 0.0
1078
1087
1079 # ancestors will be recursively updated
1088 # ancestors will be recursively updated
1080 p.save(:validate => false)
1089 p.save(:validate => false)
1081 end
1090 end
1082 end
1091 end
1083
1092
1084 # Update issues so their versions are not pointing to a
1093 # Update issues so their versions are not pointing to a
1085 # fixed_version that is not shared with the issue's project
1094 # fixed_version that is not shared with the issue's project
1086 def self.update_versions(conditions=nil)
1095 def self.update_versions(conditions=nil)
1087 # Only need to update issues with a fixed_version from
1096 # Only need to update issues with a fixed_version from
1088 # a different project and that is not systemwide shared
1097 # a different project and that is not systemwide shared
1089 Issue.scoped(:conditions => conditions).all(
1098 Issue.scoped(:conditions => conditions).all(
1090 :conditions => "#{Issue.table_name}.fixed_version_id IS NOT NULL" +
1099 :conditions => "#{Issue.table_name}.fixed_version_id IS NOT NULL" +
1091 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
1100 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
1092 " AND #{Version.table_name}.sharing <> 'system'",
1101 " AND #{Version.table_name}.sharing <> 'system'",
1093 :include => [:project, :fixed_version]
1102 :include => [:project, :fixed_version]
1094 ).each do |issue|
1103 ).each do |issue|
1095 next if issue.project.nil? || issue.fixed_version.nil?
1104 next if issue.project.nil? || issue.fixed_version.nil?
1096 unless issue.project.shared_versions.include?(issue.fixed_version)
1105 unless issue.project.shared_versions.include?(issue.fixed_version)
1097 issue.init_journal(User.current)
1106 issue.init_journal(User.current)
1098 issue.fixed_version = nil
1107 issue.fixed_version = nil
1099 issue.save
1108 issue.save
1100 end
1109 end
1101 end
1110 end
1102 end
1111 end
1103
1112
1104 # Callback on attachment deletion
1113 # Callback on attachment deletion
1105 def attachment_added(obj)
1114 def attachment_added(obj)
1106 if @current_journal && !obj.new_record?
1115 if @current_journal && !obj.new_record?
1107 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :value => obj.filename)
1116 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :value => obj.filename)
1108 end
1117 end
1109 end
1118 end
1110
1119
1111 # Callback on attachment deletion
1120 # Callback on attachment deletion
1112 def attachment_removed(obj)
1121 def attachment_removed(obj)
1113 if @current_journal && !obj.new_record?
1122 if @current_journal && !obj.new_record?
1114 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :old_value => obj.filename)
1123 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :old_value => obj.filename)
1115 @current_journal.save
1124 @current_journal.save
1116 end
1125 end
1117 end
1126 end
1118
1127
1119 # Default assignment based on category
1128 # Default assignment based on category
1120 def default_assign
1129 def default_assign
1121 if assigned_to.nil? && category && category.assigned_to
1130 if assigned_to.nil? && category && category.assigned_to
1122 self.assigned_to = category.assigned_to
1131 self.assigned_to = category.assigned_to
1123 end
1132 end
1124 end
1133 end
1125
1134
1126 # Updates start/due dates of following issues
1135 # Updates start/due dates of following issues
1127 def reschedule_following_issues
1136 def reschedule_following_issues
1128 if start_date_changed? || due_date_changed?
1137 if start_date_changed? || due_date_changed?
1129 relations_from.each do |relation|
1138 relations_from.each do |relation|
1130 relation.set_issue_to_dates
1139 relation.set_issue_to_dates
1131 end
1140 end
1132 end
1141 end
1133 end
1142 end
1134
1143
1135 # Closes duplicates if the issue is being closed
1144 # Closes duplicates if the issue is being closed
1136 def close_duplicates
1145 def close_duplicates
1137 if closing?
1146 if closing?
1138 duplicates.each do |duplicate|
1147 duplicates.each do |duplicate|
1139 # Reload is need in case the duplicate was updated by a previous duplicate
1148 # Reload is need in case the duplicate was updated by a previous duplicate
1140 duplicate.reload
1149 duplicate.reload
1141 # Don't re-close it if it's already closed
1150 # Don't re-close it if it's already closed
1142 next if duplicate.closed?
1151 next if duplicate.closed?
1143 # Same user and notes
1152 # Same user and notes
1144 if @current_journal
1153 if @current_journal
1145 duplicate.init_journal(@current_journal.user, @current_journal.notes)
1154 duplicate.init_journal(@current_journal.user, @current_journal.notes)
1146 end
1155 end
1147 duplicate.update_attribute :status, self.status
1156 duplicate.update_attribute :status, self.status
1148 end
1157 end
1149 end
1158 end
1150 end
1159 end
1151
1160
1152 # Make sure updated_on is updated when adding a note
1161 # Make sure updated_on is updated when adding a note
1153 def force_updated_on_change
1162 def force_updated_on_change
1154 if @current_journal
1163 if @current_journal
1155 self.updated_on = current_time_from_proper_timezone
1164 self.updated_on = current_time_from_proper_timezone
1156 end
1165 end
1157 end
1166 end
1158
1167
1159 # Saves the changes in a Journal
1168 # Saves the changes in a Journal
1160 # Called after_save
1169 # Called after_save
1161 def create_journal
1170 def create_journal
1162 if @current_journal
1171 if @current_journal
1163 # attributes changes
1172 # attributes changes
1164 if @attributes_before_change
1173 if @attributes_before_change
1165 (Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on)).each {|c|
1174 (Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on)).each {|c|
1166 before = @attributes_before_change[c]
1175 before = @attributes_before_change[c]
1167 after = send(c)
1176 after = send(c)
1168 next if before == after || (before.blank? && after.blank?)
1177 next if before == after || (before.blank? && after.blank?)
1169 @current_journal.details << JournalDetail.new(:property => 'attr',
1178 @current_journal.details << JournalDetail.new(:property => 'attr',
1170 :prop_key => c,
1179 :prop_key => c,
1171 :old_value => before,
1180 :old_value => before,
1172 :value => after)
1181 :value => after)
1173 }
1182 }
1174 end
1183 end
1175 if @custom_values_before_change
1184 if @custom_values_before_change
1176 # custom fields changes
1185 # custom fields changes
1177 custom_field_values.each {|c|
1186 custom_field_values.each {|c|
1178 before = @custom_values_before_change[c.custom_field_id]
1187 before = @custom_values_before_change[c.custom_field_id]
1179 after = c.value
1188 after = c.value
1180 next if before == after || (before.blank? && after.blank?)
1189 next if before == after || (before.blank? && after.blank?)
1181
1190
1182 if before.is_a?(Array) || after.is_a?(Array)
1191 if before.is_a?(Array) || after.is_a?(Array)
1183 before = [before] unless before.is_a?(Array)
1192 before = [before] unless before.is_a?(Array)
1184 after = [after] unless after.is_a?(Array)
1193 after = [after] unless after.is_a?(Array)
1185
1194
1186 # values removed
1195 # values removed
1187 (before - after).reject(&:blank?).each do |value|
1196 (before - after).reject(&:blank?).each do |value|
1188 @current_journal.details << JournalDetail.new(:property => 'cf',
1197 @current_journal.details << JournalDetail.new(:property => 'cf',
1189 :prop_key => c.custom_field_id,
1198 :prop_key => c.custom_field_id,
1190 :old_value => value,
1199 :old_value => value,
1191 :value => nil)
1200 :value => nil)
1192 end
1201 end
1193 # values added
1202 # values added
1194 (after - before).reject(&:blank?).each do |value|
1203 (after - before).reject(&:blank?).each do |value|
1195 @current_journal.details << JournalDetail.new(:property => 'cf',
1204 @current_journal.details << JournalDetail.new(:property => 'cf',
1196 :prop_key => c.custom_field_id,
1205 :prop_key => c.custom_field_id,
1197 :old_value => nil,
1206 :old_value => nil,
1198 :value => value)
1207 :value => value)
1199 end
1208 end
1200 else
1209 else
1201 @current_journal.details << JournalDetail.new(:property => 'cf',
1210 @current_journal.details << JournalDetail.new(:property => 'cf',
1202 :prop_key => c.custom_field_id,
1211 :prop_key => c.custom_field_id,
1203 :old_value => before,
1212 :old_value => before,
1204 :value => after)
1213 :value => after)
1205 end
1214 end
1206 }
1215 }
1207 end
1216 end
1208 @current_journal.save
1217 @current_journal.save
1209 # reset current journal
1218 # reset current journal
1210 init_journal @current_journal.user, @current_journal.notes
1219 init_journal @current_journal.user, @current_journal.notes
1211 end
1220 end
1212 end
1221 end
1213
1222
1214 # Query generator for selecting groups of issue counts for a project
1223 # Query generator for selecting groups of issue counts for a project
1215 # based on specific criteria
1224 # based on specific criteria
1216 #
1225 #
1217 # Options
1226 # Options
1218 # * project - Project to search in.
1227 # * project - Project to search in.
1219 # * field - String. Issue field to key off of in the grouping.
1228 # * field - String. Issue field to key off of in the grouping.
1220 # * joins - String. The table name to join against.
1229 # * joins - String. The table name to join against.
1221 def self.count_and_group_by(options)
1230 def self.count_and_group_by(options)
1222 project = options.delete(:project)
1231 project = options.delete(:project)
1223 select_field = options.delete(:field)
1232 select_field = options.delete(:field)
1224 joins = options.delete(:joins)
1233 joins = options.delete(:joins)
1225
1234
1226 where = "#{Issue.table_name}.#{select_field}=j.id"
1235 where = "#{Issue.table_name}.#{select_field}=j.id"
1227
1236
1228 ActiveRecord::Base.connection.select_all("select s.id as status_id,
1237 ActiveRecord::Base.connection.select_all("select s.id as status_id,
1229 s.is_closed as closed,
1238 s.is_closed as closed,
1230 j.id as #{select_field},
1239 j.id as #{select_field},
1231 count(#{Issue.table_name}.id) as total
1240 count(#{Issue.table_name}.id) as total
1232 from
1241 from
1233 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s, #{joins} j
1242 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s, #{joins} j
1234 where
1243 where
1235 #{Issue.table_name}.status_id=s.id
1244 #{Issue.table_name}.status_id=s.id
1236 and #{where}
1245 and #{where}
1237 and #{Issue.table_name}.project_id=#{Project.table_name}.id
1246 and #{Issue.table_name}.project_id=#{Project.table_name}.id
1238 and #{visible_condition(User.current, :project => project)}
1247 and #{visible_condition(User.current, :project => project)}
1239 group by s.id, s.is_closed, j.id")
1248 group by s.id, s.is_closed, j.id")
1240 end
1249 end
1241 end
1250 end
@@ -1,1533 +1,1542
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 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 :groups_users,
22 :groups_users,
23 :trackers, :projects_trackers,
23 :trackers, :projects_trackers,
24 :enabled_modules,
24 :enabled_modules,
25 :versions,
25 :versions,
26 :issue_statuses, :issue_categories, :issue_relations, :workflows,
26 :issue_statuses, :issue_categories, :issue_relations, :workflows,
27 :enumerations,
27 :enumerations,
28 :issues,
28 :issues,
29 :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values,
29 :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values,
30 :time_entries
30 :time_entries
31
31
32 include Redmine::I18n
32 include Redmine::I18n
33
33
34 def teardown
34 def teardown
35 User.current = nil
35 User.current = nil
36 end
36 end
37
37
38 def test_create
38 def test_create
39 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
39 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
40 :status_id => 1, :priority => IssuePriority.all.first,
40 :status_id => 1, :priority => IssuePriority.all.first,
41 :subject => 'test_create',
41 :subject => 'test_create',
42 :description => 'IssueTest#test_create', :estimated_hours => '1:30')
42 :description => 'IssueTest#test_create', :estimated_hours => '1:30')
43 assert issue.save
43 assert issue.save
44 issue.reload
44 issue.reload
45 assert_equal 1.5, issue.estimated_hours
45 assert_equal 1.5, issue.estimated_hours
46 end
46 end
47
47
48 def test_create_minimal
48 def test_create_minimal
49 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
49 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
50 :status_id => 1, :priority => IssuePriority.all.first,
50 :status_id => 1, :priority => IssuePriority.all.first,
51 :subject => 'test_create')
51 :subject => 'test_create')
52 assert issue.save
52 assert issue.save
53 assert issue.description.nil?
53 assert issue.description.nil?
54 end
54 end
55
55
56 def test_create_with_required_custom_field
56 def test_create_with_required_custom_field
57 set_language_if_valid 'en'
57 set_language_if_valid 'en'
58 field = IssueCustomField.find_by_name('Database')
58 field = IssueCustomField.find_by_name('Database')
59 field.update_attribute(:is_required, true)
59 field.update_attribute(:is_required, true)
60
60
61 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
61 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
62 :status_id => 1, :subject => 'test_create',
62 :status_id => 1, :subject => 'test_create',
63 :description => 'IssueTest#test_create_with_required_custom_field')
63 :description => 'IssueTest#test_create_with_required_custom_field')
64 assert issue.available_custom_fields.include?(field)
64 assert issue.available_custom_fields.include?(field)
65 # No value for the custom field
65 # No value for the custom field
66 assert !issue.save
66 assert !issue.save
67 assert_equal ["Database can't be blank"], issue.errors.full_messages
67 assert_equal ["Database can't be blank"], issue.errors.full_messages
68 # Blank value
68 # Blank value
69 issue.custom_field_values = { field.id => '' }
69 issue.custom_field_values = { field.id => '' }
70 assert !issue.save
70 assert !issue.save
71 assert_equal ["Database can't be blank"], issue.errors.full_messages
71 assert_equal ["Database can't be blank"], issue.errors.full_messages
72 # Invalid value
72 # Invalid value
73 issue.custom_field_values = { field.id => 'SQLServer' }
73 issue.custom_field_values = { field.id => 'SQLServer' }
74 assert !issue.save
74 assert !issue.save
75 assert_equal ["Database is not included in the list"], issue.errors.full_messages
75 assert_equal ["Database is not included in the list"], issue.errors.full_messages
76 # Valid value
76 # Valid value
77 issue.custom_field_values = { field.id => 'PostgreSQL' }
77 issue.custom_field_values = { field.id => 'PostgreSQL' }
78 assert issue.save
78 assert issue.save
79 issue.reload
79 issue.reload
80 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
80 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
81 end
81 end
82
82
83 def test_create_with_group_assignment
83 def test_create_with_group_assignment
84 with_settings :issue_group_assignment => '1' do
84 with_settings :issue_group_assignment => '1' do
85 assert Issue.new(:project_id => 2, :tracker_id => 1, :author_id => 1,
85 assert Issue.new(:project_id => 2, :tracker_id => 1, :author_id => 1,
86 :subject => 'Group assignment',
86 :subject => 'Group assignment',
87 :assigned_to_id => 11).save
87 :assigned_to_id => 11).save
88 issue = Issue.first(:order => 'id DESC')
88 issue = Issue.first(:order => 'id DESC')
89 assert_kind_of Group, issue.assigned_to
89 assert_kind_of Group, issue.assigned_to
90 assert_equal Group.find(11), issue.assigned_to
90 assert_equal Group.find(11), issue.assigned_to
91 end
91 end
92 end
92 end
93
93
94 def assert_visibility_match(user, issues)
94 def assert_visibility_match(user, issues)
95 assert_equal issues.collect(&:id).sort, Issue.all.select {|issue| issue.visible?(user)}.collect(&:id).sort
95 assert_equal issues.collect(&:id).sort, Issue.all.select {|issue| issue.visible?(user)}.collect(&:id).sort
96 end
96 end
97
97
98 def test_visible_scope_for_anonymous
98 def test_visible_scope_for_anonymous
99 # Anonymous user should see issues of public projects only
99 # Anonymous user should see issues of public projects only
100 issues = Issue.visible(User.anonymous).all
100 issues = Issue.visible(User.anonymous).all
101 assert issues.any?
101 assert issues.any?
102 assert_nil issues.detect {|issue| !issue.project.is_public?}
102 assert_nil issues.detect {|issue| !issue.project.is_public?}
103 assert_nil issues.detect {|issue| issue.is_private?}
103 assert_nil issues.detect {|issue| issue.is_private?}
104 assert_visibility_match User.anonymous, issues
104 assert_visibility_match User.anonymous, issues
105 end
105 end
106
106
107 def test_visible_scope_for_anonymous_with_own_issues_visibility
107 def test_visible_scope_for_anonymous_with_own_issues_visibility
108 Role.anonymous.update_attribute :issues_visibility, 'own'
108 Role.anonymous.update_attribute :issues_visibility, 'own'
109 Issue.create!(:project_id => 1, :tracker_id => 1,
109 Issue.create!(:project_id => 1, :tracker_id => 1,
110 :author_id => User.anonymous.id,
110 :author_id => User.anonymous.id,
111 :subject => 'Issue by anonymous')
111 :subject => 'Issue by anonymous')
112
112
113 issues = Issue.visible(User.anonymous).all
113 issues = Issue.visible(User.anonymous).all
114 assert issues.any?
114 assert issues.any?
115 assert_nil issues.detect {|issue| issue.author != User.anonymous}
115 assert_nil issues.detect {|issue| issue.author != User.anonymous}
116 assert_visibility_match User.anonymous, issues
116 assert_visibility_match User.anonymous, issues
117 end
117 end
118
118
119 def test_visible_scope_for_anonymous_without_view_issues_permissions
119 def test_visible_scope_for_anonymous_without_view_issues_permissions
120 # Anonymous user should not see issues without permission
120 # Anonymous user should not see issues without permission
121 Role.anonymous.remove_permission!(:view_issues)
121 Role.anonymous.remove_permission!(:view_issues)
122 issues = Issue.visible(User.anonymous).all
122 issues = Issue.visible(User.anonymous).all
123 assert issues.empty?
123 assert issues.empty?
124 assert_visibility_match User.anonymous, issues
124 assert_visibility_match User.anonymous, issues
125 end
125 end
126
126
127 def test_visible_scope_for_non_member
127 def test_visible_scope_for_non_member
128 user = User.find(9)
128 user = User.find(9)
129 assert user.projects.empty?
129 assert user.projects.empty?
130 # Non member user should see issues of public projects only
130 # Non member user should see issues of public projects only
131 issues = Issue.visible(user).all
131 issues = Issue.visible(user).all
132 assert issues.any?
132 assert issues.any?
133 assert_nil issues.detect {|issue| !issue.project.is_public?}
133 assert_nil issues.detect {|issue| !issue.project.is_public?}
134 assert_nil issues.detect {|issue| issue.is_private?}
134 assert_nil issues.detect {|issue| issue.is_private?}
135 assert_visibility_match user, issues
135 assert_visibility_match user, issues
136 end
136 end
137
137
138 def test_visible_scope_for_non_member_with_own_issues_visibility
138 def test_visible_scope_for_non_member_with_own_issues_visibility
139 Role.non_member.update_attribute :issues_visibility, 'own'
139 Role.non_member.update_attribute :issues_visibility, 'own'
140 Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 9, :subject => 'Issue by non member')
140 Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 9, :subject => 'Issue by non member')
141 user = User.find(9)
141 user = User.find(9)
142
142
143 issues = Issue.visible(user).all
143 issues = Issue.visible(user).all
144 assert issues.any?
144 assert issues.any?
145 assert_nil issues.detect {|issue| issue.author != user}
145 assert_nil issues.detect {|issue| issue.author != user}
146 assert_visibility_match user, issues
146 assert_visibility_match user, issues
147 end
147 end
148
148
149 def test_visible_scope_for_non_member_without_view_issues_permissions
149 def test_visible_scope_for_non_member_without_view_issues_permissions
150 # Non member user should not see issues without permission
150 # Non member user should not see issues without permission
151 Role.non_member.remove_permission!(:view_issues)
151 Role.non_member.remove_permission!(:view_issues)
152 user = User.find(9)
152 user = User.find(9)
153 assert user.projects.empty?
153 assert user.projects.empty?
154 issues = Issue.visible(user).all
154 issues = Issue.visible(user).all
155 assert issues.empty?
155 assert issues.empty?
156 assert_visibility_match user, issues
156 assert_visibility_match user, issues
157 end
157 end
158
158
159 def test_visible_scope_for_member
159 def test_visible_scope_for_member
160 user = User.find(9)
160 user = User.find(9)
161 # User should see issues of projects for which he has view_issues permissions only
161 # User should see issues of projects for which he has view_issues permissions only
162 Role.non_member.remove_permission!(:view_issues)
162 Role.non_member.remove_permission!(:view_issues)
163 Member.create!(:principal => user, :project_id => 3, :role_ids => [2])
163 Member.create!(:principal => user, :project_id => 3, :role_ids => [2])
164 issues = Issue.visible(user).all
164 issues = Issue.visible(user).all
165 assert issues.any?
165 assert issues.any?
166 assert_nil issues.detect {|issue| issue.project_id != 3}
166 assert_nil issues.detect {|issue| issue.project_id != 3}
167 assert_nil issues.detect {|issue| issue.is_private?}
167 assert_nil issues.detect {|issue| issue.is_private?}
168 assert_visibility_match user, issues
168 assert_visibility_match user, issues
169 end
169 end
170
170
171 def test_visible_scope_for_member_with_groups_should_return_assigned_issues
171 def test_visible_scope_for_member_with_groups_should_return_assigned_issues
172 user = User.find(8)
172 user = User.find(8)
173 assert user.groups.any?
173 assert user.groups.any?
174 Member.create!(:principal => user.groups.first, :project_id => 1, :role_ids => [2])
174 Member.create!(:principal => user.groups.first, :project_id => 1, :role_ids => [2])
175 Role.non_member.remove_permission!(:view_issues)
175 Role.non_member.remove_permission!(:view_issues)
176
176
177 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3,
177 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3,
178 :status_id => 1, :priority => IssuePriority.all.first,
178 :status_id => 1, :priority => IssuePriority.all.first,
179 :subject => 'Assignment test',
179 :subject => 'Assignment test',
180 :assigned_to => user.groups.first,
180 :assigned_to => user.groups.first,
181 :is_private => true)
181 :is_private => true)
182
182
183 Role.find(2).update_attribute :issues_visibility, 'default'
183 Role.find(2).update_attribute :issues_visibility, 'default'
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
187
188 Role.find(2).update_attribute :issues_visibility, 'own'
188 Role.find(2).update_attribute :issues_visibility, 'own'
189 issues = Issue.visible(User.find(8)).all
189 issues = Issue.visible(User.find(8)).all
190 assert issues.any?
190 assert issues.any?
191 assert issues.include?(issue)
191 assert issues.include?(issue)
192 end
192 end
193
193
194 def test_visible_scope_for_admin
194 def test_visible_scope_for_admin
195 user = User.find(1)
195 user = User.find(1)
196 user.members.each(&:destroy)
196 user.members.each(&:destroy)
197 assert user.projects.empty?
197 assert user.projects.empty?
198 issues = Issue.visible(user).all
198 issues = Issue.visible(user).all
199 assert issues.any?
199 assert issues.any?
200 # Admin should see issues on private projects that he does not belong to
200 # Admin should see issues on private projects that he does not belong to
201 assert issues.detect {|issue| !issue.project.is_public?}
201 assert issues.detect {|issue| !issue.project.is_public?}
202 # Admin should see private issues of other users
202 # Admin should see private issues of other users
203 assert issues.detect {|issue| issue.is_private? && issue.author != user}
203 assert issues.detect {|issue| issue.is_private? && issue.author != user}
204 assert_visibility_match user, issues
204 assert_visibility_match user, issues
205 end
205 end
206
206
207 def test_visible_scope_with_project
207 def test_visible_scope_with_project
208 project = Project.find(1)
208 project = Project.find(1)
209 issues = Issue.visible(User.find(2), :project => project).all
209 issues = Issue.visible(User.find(2), :project => project).all
210 projects = issues.collect(&:project).uniq
210 projects = issues.collect(&:project).uniq
211 assert_equal 1, projects.size
211 assert_equal 1, projects.size
212 assert_equal project, projects.first
212 assert_equal project, projects.first
213 end
213 end
214
214
215 def test_visible_scope_with_project_and_subprojects
215 def test_visible_scope_with_project_and_subprojects
216 project = Project.find(1)
216 project = Project.find(1)
217 issues = Issue.visible(User.find(2), :project => project, :with_subprojects => true).all
217 issues = Issue.visible(User.find(2), :project => project, :with_subprojects => true).all
218 projects = issues.collect(&:project).uniq
218 projects = issues.collect(&:project).uniq
219 assert projects.size > 1
219 assert projects.size > 1
220 assert_equal [], projects.select {|p| !p.is_or_is_descendant_of?(project)}
220 assert_equal [], projects.select {|p| !p.is_or_is_descendant_of?(project)}
221 end
221 end
222
222
223 def test_visible_and_nested_set_scopes
223 def test_visible_and_nested_set_scopes
224 assert_equal 0, Issue.find(1).descendants.visible.all.size
224 assert_equal 0, Issue.find(1).descendants.visible.all.size
225 end
225 end
226
226
227 def test_open_scope
227 def test_open_scope
228 issues = Issue.open.all
228 issues = Issue.open.all
229 assert_nil issues.detect(&:closed?)
229 assert_nil issues.detect(&:closed?)
230 end
230 end
231
231
232 def test_open_scope_with_arg
232 def test_open_scope_with_arg
233 issues = Issue.open(false).all
233 issues = Issue.open(false).all
234 assert_equal issues, issues.select(&:closed?)
234 assert_equal issues, issues.select(&:closed?)
235 end
235 end
236
236
237 def test_errors_full_messages_should_include_custom_fields_errors
237 def test_errors_full_messages_should_include_custom_fields_errors
238 field = IssueCustomField.find_by_name('Database')
238 field = IssueCustomField.find_by_name('Database')
239
239
240 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
240 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
241 :status_id => 1, :subject => 'test_create',
241 :status_id => 1, :subject => 'test_create',
242 :description => 'IssueTest#test_create_with_required_custom_field')
242 :description => 'IssueTest#test_create_with_required_custom_field')
243 assert issue.available_custom_fields.include?(field)
243 assert issue.available_custom_fields.include?(field)
244 # Invalid value
244 # Invalid value
245 issue.custom_field_values = { field.id => 'SQLServer' }
245 issue.custom_field_values = { field.id => 'SQLServer' }
246
246
247 assert !issue.valid?
247 assert !issue.valid?
248 assert_equal 1, issue.errors.full_messages.size
248 assert_equal 1, issue.errors.full_messages.size
249 assert_equal "Database #{I18n.translate('activerecord.errors.messages.inclusion')}",
249 assert_equal "Database #{I18n.translate('activerecord.errors.messages.inclusion')}",
250 issue.errors.full_messages.first
250 issue.errors.full_messages.first
251 end
251 end
252
252
253 def test_update_issue_with_required_custom_field
253 def test_update_issue_with_required_custom_field
254 field = IssueCustomField.find_by_name('Database')
254 field = IssueCustomField.find_by_name('Database')
255 field.update_attribute(:is_required, true)
255 field.update_attribute(:is_required, true)
256
256
257 issue = Issue.find(1)
257 issue = Issue.find(1)
258 assert_nil issue.custom_value_for(field)
258 assert_nil issue.custom_value_for(field)
259 assert issue.available_custom_fields.include?(field)
259 assert issue.available_custom_fields.include?(field)
260 # No change to custom values, issue can be saved
260 # No change to custom values, issue can be saved
261 assert issue.save
261 assert issue.save
262 # Blank value
262 # Blank value
263 issue.custom_field_values = { field.id => '' }
263 issue.custom_field_values = { field.id => '' }
264 assert !issue.save
264 assert !issue.save
265 # Valid value
265 # Valid value
266 issue.custom_field_values = { field.id => 'PostgreSQL' }
266 issue.custom_field_values = { field.id => 'PostgreSQL' }
267 assert issue.save
267 assert issue.save
268 issue.reload
268 issue.reload
269 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
269 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
270 end
270 end
271
271
272 def test_should_not_update_attributes_if_custom_fields_validation_fails
272 def test_should_not_update_attributes_if_custom_fields_validation_fails
273 issue = Issue.find(1)
273 issue = Issue.find(1)
274 field = IssueCustomField.find_by_name('Database')
274 field = IssueCustomField.find_by_name('Database')
275 assert issue.available_custom_fields.include?(field)
275 assert issue.available_custom_fields.include?(field)
276
276
277 issue.custom_field_values = { field.id => 'Invalid' }
277 issue.custom_field_values = { field.id => 'Invalid' }
278 issue.subject = 'Should be not be saved'
278 issue.subject = 'Should be not be saved'
279 assert !issue.save
279 assert !issue.save
280
280
281 issue.reload
281 issue.reload
282 assert_equal "Can't print recipes", issue.subject
282 assert_equal "Can't print recipes", issue.subject
283 end
283 end
284
284
285 def test_should_not_recreate_custom_values_objects_on_update
285 def test_should_not_recreate_custom_values_objects_on_update
286 field = IssueCustomField.find_by_name('Database')
286 field = IssueCustomField.find_by_name('Database')
287
287
288 issue = Issue.find(1)
288 issue = Issue.find(1)
289 issue.custom_field_values = { field.id => 'PostgreSQL' }
289 issue.custom_field_values = { field.id => 'PostgreSQL' }
290 assert issue.save
290 assert issue.save
291 custom_value = issue.custom_value_for(field)
291 custom_value = issue.custom_value_for(field)
292 issue.reload
292 issue.reload
293 issue.custom_field_values = { field.id => 'MySQL' }
293 issue.custom_field_values = { field.id => 'MySQL' }
294 assert issue.save
294 assert issue.save
295 issue.reload
295 issue.reload
296 assert_equal custom_value.id, issue.custom_value_for(field).id
296 assert_equal custom_value.id, issue.custom_value_for(field).id
297 end
297 end
298
298
299 def test_should_not_update_custom_fields_on_changing_tracker_with_different_custom_fields
299 def test_should_not_update_custom_fields_on_changing_tracker_with_different_custom_fields
300 issue = Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => 'Test', :custom_field_values => {'2' => 'Test'})
300 issue = Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => 'Test', :custom_field_values => {'2' => 'Test'})
301 assert !Tracker.find(2).custom_field_ids.include?(2)
301 assert !Tracker.find(2).custom_field_ids.include?(2)
302
302
303 issue = Issue.find(issue.id)
303 issue = Issue.find(issue.id)
304 issue.attributes = {:tracker_id => 2, :custom_field_values => {'1' => ''}}
304 issue.attributes = {:tracker_id => 2, :custom_field_values => {'1' => ''}}
305
305
306 issue = Issue.find(issue.id)
306 issue = Issue.find(issue.id)
307 custom_value = issue.custom_value_for(2)
307 custom_value = issue.custom_value_for(2)
308 assert_not_nil custom_value
308 assert_not_nil custom_value
309 assert_equal 'Test', custom_value.value
309 assert_equal 'Test', custom_value.value
310 end
310 end
311
311
312 def test_assigning_tracker_id_should_reload_custom_fields_values
312 def test_assigning_tracker_id_should_reload_custom_fields_values
313 issue = Issue.new(:project => Project.find(1))
313 issue = Issue.new(:project => Project.find(1))
314 assert issue.custom_field_values.empty?
314 assert issue.custom_field_values.empty?
315 issue.tracker_id = 1
315 issue.tracker_id = 1
316 assert issue.custom_field_values.any?
316 assert issue.custom_field_values.any?
317 end
317 end
318
318
319 def test_assigning_attributes_should_assign_project_and_tracker_first
319 def test_assigning_attributes_should_assign_project_and_tracker_first
320 seq = sequence('seq')
320 seq = sequence('seq')
321 issue = Issue.new
321 issue = Issue.new
322 issue.expects(:project_id=).in_sequence(seq)
322 issue.expects(:project_id=).in_sequence(seq)
323 issue.expects(:tracker_id=).in_sequence(seq)
323 issue.expects(:tracker_id=).in_sequence(seq)
324 issue.expects(:subject=).in_sequence(seq)
324 issue.expects(:subject=).in_sequence(seq)
325 issue.attributes = {:tracker_id => 2, :project_id => 1, :subject => 'Test'}
325 issue.attributes = {:tracker_id => 2, :project_id => 1, :subject => 'Test'}
326 end
326 end
327
327
328 def test_assigning_tracker_and_custom_fields_should_assign_custom_fields
328 def test_assigning_tracker_and_custom_fields_should_assign_custom_fields
329 attributes = ActiveSupport::OrderedHash.new
329 attributes = ActiveSupport::OrderedHash.new
330 attributes['custom_field_values'] = { '1' => 'MySQL' }
330 attributes['custom_field_values'] = { '1' => 'MySQL' }
331 attributes['tracker_id'] = '1'
331 attributes['tracker_id'] = '1'
332 issue = Issue.new(:project => Project.find(1))
332 issue = Issue.new(:project => Project.find(1))
333 issue.attributes = attributes
333 issue.attributes = attributes
334 assert_equal 'MySQL', issue.custom_field_value(1)
334 assert_equal 'MySQL', issue.custom_field_value(1)
335 end
335 end
336
336
337 def test_should_update_issue_with_disabled_tracker
337 def test_should_update_issue_with_disabled_tracker
338 p = Project.find(1)
338 p = Project.find(1)
339 issue = Issue.find(1)
339 issue = Issue.find(1)
340
340
341 p.trackers.delete(issue.tracker)
341 p.trackers.delete(issue.tracker)
342 assert !p.trackers.include?(issue.tracker)
342 assert !p.trackers.include?(issue.tracker)
343
343
344 issue.reload
344 issue.reload
345 issue.subject = 'New subject'
345 issue.subject = 'New subject'
346 assert issue.save
346 assert issue.save
347 end
347 end
348
348
349 def test_should_not_set_a_disabled_tracker
349 def test_should_not_set_a_disabled_tracker
350 p = Project.find(1)
350 p = Project.find(1)
351 p.trackers.delete(Tracker.find(2))
351 p.trackers.delete(Tracker.find(2))
352
352
353 issue = Issue.find(1)
353 issue = Issue.find(1)
354 issue.tracker_id = 2
354 issue.tracker_id = 2
355 issue.subject = 'New subject'
355 issue.subject = 'New subject'
356 assert !issue.save
356 assert !issue.save
357 assert_not_nil issue.errors[:tracker_id]
357 assert_not_nil issue.errors[:tracker_id]
358 end
358 end
359
359
360 def test_category_based_assignment
360 def test_category_based_assignment
361 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3,
361 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3,
362 :status_id => 1, :priority => IssuePriority.all.first,
362 :status_id => 1, :priority => IssuePriority.all.first,
363 :subject => 'Assignment test',
363 :subject => 'Assignment test',
364 :description => 'Assignment test', :category_id => 1)
364 :description => 'Assignment test', :category_id => 1)
365 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
365 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
366 end
366 end
367
367
368 def test_new_statuses_allowed_to
368 def test_new_statuses_allowed_to
369 WorkflowTransition.delete_all
369 WorkflowTransition.delete_all
370
370
371 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 2, :author => false, :assignee => false)
371 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 2, :author => false, :assignee => false)
372 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 3, :author => true, :assignee => false)
372 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 3, :author => true, :assignee => false)
373 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 4, :author => false, :assignee => true)
373 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 4, :author => false, :assignee => true)
374 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 5, :author => true, :assignee => true)
374 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 5, :author => true, :assignee => true)
375 status = IssueStatus.find(1)
375 status = IssueStatus.find(1)
376 role = Role.find(1)
376 role = Role.find(1)
377 tracker = Tracker.find(1)
377 tracker = Tracker.find(1)
378 user = User.find(2)
378 user = User.find(2)
379
379
380 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1, :author_id => 1)
380 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1, :author_id => 1)
381 assert_equal [1, 2], issue.new_statuses_allowed_to(user).map(&:id)
381 assert_equal [1, 2], issue.new_statuses_allowed_to(user).map(&:id)
382
382
383 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1, :author => user)
383 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1, :author => user)
384 assert_equal [1, 2, 3, 5], issue.new_statuses_allowed_to(user).map(&:id)
384 assert_equal [1, 2, 3, 5], issue.new_statuses_allowed_to(user).map(&:id)
385
385
386 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1, :author_id => 1, :assigned_to => user)
386 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1, :author_id => 1, :assigned_to => user)
387 assert_equal [1, 2, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
387 assert_equal [1, 2, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
388
388
389 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1, :author => user, :assigned_to => user)
389 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1, :author => user, :assigned_to => user)
390 assert_equal [1, 2, 3, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
390 assert_equal [1, 2, 3, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
391 end
391 end
392
392
393 def test_new_statuses_allowed_to_should_return_all_transitions_for_admin
393 def test_new_statuses_allowed_to_should_return_all_transitions_for_admin
394 admin = User.find(1)
394 admin = User.find(1)
395 issue = Issue.find(1)
395 issue = Issue.find(1)
396 assert !admin.member_of?(issue.project)
396 assert !admin.member_of?(issue.project)
397 expected_statuses = [issue.status] + WorkflowTransition.find_all_by_old_status_id(issue.status_id).map(&:new_status).uniq.sort
397 expected_statuses = [issue.status] + WorkflowTransition.find_all_by_old_status_id(issue.status_id).map(&:new_status).uniq.sort
398
398
399 assert_equal expected_statuses, issue.new_statuses_allowed_to(admin)
399 assert_equal expected_statuses, issue.new_statuses_allowed_to(admin)
400 end
400 end
401
401
402 def test_new_statuses_allowed_to_should_return_default_and_current_status_when_copying
402 def test_new_statuses_allowed_to_should_return_default_and_current_status_when_copying
403 issue = Issue.find(1).copy
403 issue = Issue.find(1).copy
404 assert_equal [1], issue.new_statuses_allowed_to(User.find(2)).map(&:id)
404 assert_equal [1], issue.new_statuses_allowed_to(User.find(2)).map(&:id)
405
405
406 issue = Issue.find(2).copy
406 issue = Issue.find(2).copy
407 assert_equal [1, 2], issue.new_statuses_allowed_to(User.find(2)).map(&:id)
407 assert_equal [1, 2], issue.new_statuses_allowed_to(User.find(2)).map(&:id)
408 end
408 end
409
409
410 def test_safe_attributes_names_should_not_include_disabled_field
410 def test_safe_attributes_names_should_not_include_disabled_field
411 tracker = Tracker.new(:core_fields => %w(assigned_to_id fixed_version_id))
411 tracker = Tracker.new(:core_fields => %w(assigned_to_id fixed_version_id))
412
412
413 issue = Issue.new(:tracker => tracker)
413 issue = Issue.new(:tracker => tracker)
414 assert_include 'tracker_id', issue.safe_attribute_names
414 assert_include 'tracker_id', issue.safe_attribute_names
415 assert_include 'status_id', issue.safe_attribute_names
415 assert_include 'status_id', issue.safe_attribute_names
416 assert_include 'subject', issue.safe_attribute_names
416 assert_include 'subject', issue.safe_attribute_names
417 assert_include 'description', issue.safe_attribute_names
417 assert_include 'description', issue.safe_attribute_names
418 assert_include 'custom_field_values', issue.safe_attribute_names
418 assert_include 'custom_field_values', issue.safe_attribute_names
419 assert_include 'custom_fields', issue.safe_attribute_names
419 assert_include 'custom_fields', issue.safe_attribute_names
420 assert_include 'lock_version', issue.safe_attribute_names
420 assert_include 'lock_version', issue.safe_attribute_names
421
421
422 tracker.core_fields.each do |field|
422 tracker.core_fields.each do |field|
423 assert_include field, issue.safe_attribute_names
423 assert_include field, issue.safe_attribute_names
424 end
424 end
425
425
426 tracker.disabled_core_fields.each do |field|
426 tracker.disabled_core_fields.each do |field|
427 assert_not_include field, issue.safe_attribute_names
427 assert_not_include field, issue.safe_attribute_names
428 end
428 end
429 end
429 end
430
430
431 def test_safe_attributes_should_ignore_disabled_fields
431 def test_safe_attributes_should_ignore_disabled_fields
432 tracker = Tracker.find(1)
432 tracker = Tracker.find(1)
433 tracker.core_fields = %w(assigned_to_id due_date)
433 tracker.core_fields = %w(assigned_to_id due_date)
434 tracker.save!
434 tracker.save!
435
435
436 issue = Issue.new(:tracker => tracker)
436 issue = Issue.new(:tracker => tracker)
437 issue.safe_attributes = {'start_date' => '2012-07-14', 'due_date' => '2012-07-14'}
437 issue.safe_attributes = {'start_date' => '2012-07-14', 'due_date' => '2012-07-14'}
438 assert_nil issue.start_date
438 assert_nil issue.start_date
439 assert_equal Date.parse('2012-07-14'), issue.due_date
439 assert_equal Date.parse('2012-07-14'), issue.due_date
440 end
440 end
441
441
442 def test_safe_attributes_should_accept_target_tracker_enabled_fields
442 def test_safe_attributes_should_accept_target_tracker_enabled_fields
443 source = Tracker.find(1)
443 source = Tracker.find(1)
444 source.core_fields = []
444 source.core_fields = []
445 source.save!
445 source.save!
446 target = Tracker.find(2)
446 target = Tracker.find(2)
447 target.core_fields = %w(assigned_to_id due_date)
447 target.core_fields = %w(assigned_to_id due_date)
448 target.save!
448 target.save!
449
449
450 issue = Issue.new(:tracker => source)
450 issue = Issue.new(:tracker => source)
451 issue.safe_attributes = {'tracker_id' => 2, 'due_date' => '2012-07-14'}
451 issue.safe_attributes = {'tracker_id' => 2, 'due_date' => '2012-07-14'}
452 assert_equal target, issue.tracker
452 assert_equal target, issue.tracker
453 assert_equal Date.parse('2012-07-14'), issue.due_date
453 assert_equal Date.parse('2012-07-14'), issue.due_date
454 end
454 end
455
455
456 def test_safe_attributes_should_not_include_readonly_fields
456 def test_safe_attributes_should_not_include_readonly_fields
457 WorkflowPermission.delete_all
457 WorkflowPermission.delete_all
458 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => 'due_date', :rule => 'readonly')
458 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => 'due_date', :rule => 'readonly')
459 user = User.find(2)
459 user = User.find(2)
460
460
461 issue = Issue.new(:project_id => 1, :tracker_id => 1)
461 issue = Issue.new(:project_id => 1, :tracker_id => 1)
462 assert_equal %w(due_date), issue.read_only_attribute_names(user)
462 assert_equal %w(due_date), issue.read_only_attribute_names(user)
463 assert_not_include 'due_date', issue.safe_attribute_names(user)
463 assert_not_include 'due_date', issue.safe_attribute_names(user)
464
464
465 issue.send :safe_attributes=, {'start_date' => '2012-07-14', 'due_date' => '2012-07-14'}, user
465 issue.send :safe_attributes=, {'start_date' => '2012-07-14', 'due_date' => '2012-07-14'}, user
466 assert_equal Date.parse('2012-07-14'), issue.start_date
466 assert_equal Date.parse('2012-07-14'), issue.start_date
467 assert_nil issue.due_date
467 assert_nil issue.due_date
468 end
468 end
469
469
470 def test_safe_attributes_should_not_include_readonly_custom_fields
470 def test_safe_attributes_should_not_include_readonly_custom_fields
471 cf1 = IssueCustomField.create!(:name => 'Writable field', :field_format => 'string', :is_for_all => true, :tracker_ids => [1])
471 cf1 = IssueCustomField.create!(:name => 'Writable field', :field_format => 'string', :is_for_all => true, :tracker_ids => [1])
472 cf2 = IssueCustomField.create!(:name => 'Readonly field', :field_format => 'string', :is_for_all => true, :tracker_ids => [1])
472 cf2 = IssueCustomField.create!(:name => 'Readonly field', :field_format => 'string', :is_for_all => true, :tracker_ids => [1])
473
473
474 WorkflowPermission.delete_all
474 WorkflowPermission.delete_all
475 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => cf2.id.to_s, :rule => 'readonly')
475 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => cf2.id.to_s, :rule => 'readonly')
476 user = User.find(2)
476 user = User.find(2)
477
477
478 issue = Issue.new(:project_id => 1, :tracker_id => 1)
478 issue = Issue.new(:project_id => 1, :tracker_id => 1)
479 assert_equal [cf2.id.to_s], issue.read_only_attribute_names(user)
479 assert_equal [cf2.id.to_s], issue.read_only_attribute_names(user)
480 assert_not_include cf2.id.to_s, issue.safe_attribute_names(user)
480 assert_not_include cf2.id.to_s, issue.safe_attribute_names(user)
481
481
482 issue.send :safe_attributes=, {'custom_field_values' => {cf1.id.to_s => 'value1', cf2.id.to_s => 'value2'}}, user
482 issue.send :safe_attributes=, {'custom_field_values' => {cf1.id.to_s => 'value1', cf2.id.to_s => 'value2'}}, user
483 assert_equal 'value1', issue.custom_field_value(cf1)
483 assert_equal 'value1', issue.custom_field_value(cf1)
484 assert_nil issue.custom_field_value(cf2)
484 assert_nil issue.custom_field_value(cf2)
485
485
486 issue.send :safe_attributes=, {'custom_fields' => [{'id' => cf1.id.to_s, 'value' => 'valuea'}, {'id' => cf2.id.to_s, 'value' => 'valueb'}]}, user
486 issue.send :safe_attributes=, {'custom_fields' => [{'id' => cf1.id.to_s, 'value' => 'valuea'}, {'id' => cf2.id.to_s, 'value' => 'valueb'}]}, user
487 assert_equal 'valuea', issue.custom_field_value(cf1)
487 assert_equal 'valuea', issue.custom_field_value(cf1)
488 assert_nil issue.custom_field_value(cf2)
488 assert_nil issue.custom_field_value(cf2)
489 end
489 end
490
490
491 def test_editable_custom_field_values_should_return_non_readonly_custom_values
491 def test_editable_custom_field_values_should_return_non_readonly_custom_values
492 cf1 = IssueCustomField.create!(:name => 'Writable field', :field_format => 'string', :is_for_all => true, :tracker_ids => [1, 2])
492 cf1 = IssueCustomField.create!(:name => 'Writable field', :field_format => 'string', :is_for_all => true, :tracker_ids => [1, 2])
493 cf2 = IssueCustomField.create!(:name => 'Readonly field', :field_format => 'string', :is_for_all => true, :tracker_ids => [1, 2])
493 cf2 = IssueCustomField.create!(:name => 'Readonly field', :field_format => 'string', :is_for_all => true, :tracker_ids => [1, 2])
494
494
495 WorkflowPermission.delete_all
495 WorkflowPermission.delete_all
496 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => cf2.id.to_s, :rule => 'readonly')
496 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => cf2.id.to_s, :rule => 'readonly')
497 user = User.find(2)
497 user = User.find(2)
498
498
499 issue = Issue.new(:project_id => 1, :tracker_id => 1)
499 issue = Issue.new(:project_id => 1, :tracker_id => 1)
500 values = issue.editable_custom_field_values(user)
500 values = issue.editable_custom_field_values(user)
501 assert values.detect {|value| value.custom_field == cf1}
501 assert values.detect {|value| value.custom_field == cf1}
502 assert_nil values.detect {|value| value.custom_field == cf2}
502 assert_nil values.detect {|value| value.custom_field == cf2}
503
503
504 issue.tracker_id = 2
504 issue.tracker_id = 2
505 values = issue.editable_custom_field_values(user)
505 values = issue.editable_custom_field_values(user)
506 assert values.detect {|value| value.custom_field == cf1}
506 assert values.detect {|value| value.custom_field == cf1}
507 assert values.detect {|value| value.custom_field == cf2}
507 assert values.detect {|value| value.custom_field == cf2}
508 end
508 end
509
509
510 def test_safe_attributes_should_accept_target_tracker_writable_fields
510 def test_safe_attributes_should_accept_target_tracker_writable_fields
511 WorkflowPermission.delete_all
511 WorkflowPermission.delete_all
512 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => 'due_date', :rule => 'readonly')
512 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => 'due_date', :rule => 'readonly')
513 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2, :role_id => 1, :field_name => 'start_date', :rule => 'readonly')
513 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2, :role_id => 1, :field_name => 'start_date', :rule => 'readonly')
514 user = User.find(2)
514 user = User.find(2)
515
515
516 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
516 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
517
517
518 issue.send :safe_attributes=, {'start_date' => '2012-07-12', 'due_date' => '2012-07-14'}, user
518 issue.send :safe_attributes=, {'start_date' => '2012-07-12', 'due_date' => '2012-07-14'}, user
519 assert_equal Date.parse('2012-07-12'), issue.start_date
519 assert_equal Date.parse('2012-07-12'), issue.start_date
520 assert_nil issue.due_date
520 assert_nil issue.due_date
521
521
522 issue.send :safe_attributes=, {'start_date' => '2012-07-15', 'due_date' => '2012-07-16', 'tracker_id' => 2}, user
522 issue.send :safe_attributes=, {'start_date' => '2012-07-15', 'due_date' => '2012-07-16', 'tracker_id' => 2}, user
523 assert_equal Date.parse('2012-07-12'), issue.start_date
523 assert_equal Date.parse('2012-07-12'), issue.start_date
524 assert_equal Date.parse('2012-07-16'), issue.due_date
524 assert_equal Date.parse('2012-07-16'), issue.due_date
525 end
525 end
526
526
527 def test_safe_attributes_should_accept_target_status_writable_fields
527 def test_safe_attributes_should_accept_target_status_writable_fields
528 WorkflowPermission.delete_all
528 WorkflowPermission.delete_all
529 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => 'due_date', :rule => 'readonly')
529 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => 'due_date', :rule => 'readonly')
530 WorkflowPermission.create!(:old_status_id => 2, :tracker_id => 1, :role_id => 1, :field_name => 'start_date', :rule => 'readonly')
530 WorkflowPermission.create!(:old_status_id => 2, :tracker_id => 1, :role_id => 1, :field_name => 'start_date', :rule => 'readonly')
531 user = User.find(2)
531 user = User.find(2)
532
532
533 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
533 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
534
534
535 issue.send :safe_attributes=, {'start_date' => '2012-07-12', 'due_date' => '2012-07-14'}, user
535 issue.send :safe_attributes=, {'start_date' => '2012-07-12', 'due_date' => '2012-07-14'}, user
536 assert_equal Date.parse('2012-07-12'), issue.start_date
536 assert_equal Date.parse('2012-07-12'), issue.start_date
537 assert_nil issue.due_date
537 assert_nil issue.due_date
538
538
539 issue.send :safe_attributes=, {'start_date' => '2012-07-15', 'due_date' => '2012-07-16', 'status_id' => 2}, user
539 issue.send :safe_attributes=, {'start_date' => '2012-07-15', 'due_date' => '2012-07-16', 'status_id' => 2}, user
540 assert_equal Date.parse('2012-07-12'), issue.start_date
540 assert_equal Date.parse('2012-07-12'), issue.start_date
541 assert_equal Date.parse('2012-07-16'), issue.due_date
541 assert_equal Date.parse('2012-07-16'), issue.due_date
542 end
542 end
543
543
544 def test_required_attributes_should_be_validated
544 def test_required_attributes_should_be_validated
545 cf = IssueCustomField.create!(:name => 'Foo', :field_format => 'string', :is_for_all => true, :tracker_ids => [1, 2])
545 cf = IssueCustomField.create!(:name => 'Foo', :field_format => 'string', :is_for_all => true, :tracker_ids => [1, 2])
546
546
547 WorkflowPermission.delete_all
547 WorkflowPermission.delete_all
548 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => 'due_date', :rule => 'required')
548 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => 'due_date', :rule => 'required')
549 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => 'category_id', :rule => 'required')
549 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => 'category_id', :rule => 'required')
550 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => cf.id.to_s, :rule => 'required')
550 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => cf.id.to_s, :rule => 'required')
551
551
552 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2, :role_id => 1, :field_name => 'start_date', :rule => 'required')
552 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2, :role_id => 1, :field_name => 'start_date', :rule => 'required')
553 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2, :role_id => 1, :field_name => cf.id.to_s, :rule => 'required')
553 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2, :role_id => 1, :field_name => cf.id.to_s, :rule => 'required')
554 user = User.find(2)
554 user = User.find(2)
555
555
556 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1, :subject => 'Required fields', :author => user)
556 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1, :subject => 'Required fields', :author => user)
557 assert_equal [cf.id.to_s, "category_id", "due_date"], issue.required_attribute_names(user).sort
557 assert_equal [cf.id.to_s, "category_id", "due_date"], issue.required_attribute_names(user).sort
558 assert !issue.save, "Issue was saved"
558 assert !issue.save, "Issue was saved"
559 assert_equal ["Category can't be blank", "Due date can't be blank", "Foo can't be blank"], issue.errors.full_messages.sort
559 assert_equal ["Category can't be blank", "Due date can't be blank", "Foo can't be blank"], issue.errors.full_messages.sort
560
560
561 issue.tracker_id = 2
561 issue.tracker_id = 2
562 assert_equal [cf.id.to_s, "start_date"], issue.required_attribute_names(user).sort
562 assert_equal [cf.id.to_s, "start_date"], issue.required_attribute_names(user).sort
563 assert !issue.save, "Issue was saved"
563 assert !issue.save, "Issue was saved"
564 assert_equal ["Foo can't be blank", "Start date can't be blank"], issue.errors.full_messages.sort
564 assert_equal ["Foo can't be blank", "Start date can't be blank"], issue.errors.full_messages.sort
565
565
566 issue.start_date = Date.today
566 issue.start_date = Date.today
567 issue.custom_field_values = {cf.id.to_s => 'bar'}
567 issue.custom_field_values = {cf.id.to_s => 'bar'}
568 assert issue.save
568 assert issue.save
569 end
569 end
570
570
571 def test_required_attribute_names_for_multiple_roles_should_intersect_rules
571 def test_required_attribute_names_for_multiple_roles_should_intersect_rules
572 WorkflowPermission.delete_all
572 WorkflowPermission.delete_all
573 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => 'due_date', :rule => 'required')
573 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => 'due_date', :rule => 'required')
574 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => 'start_date', :rule => 'required')
574 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => 'start_date', :rule => 'required')
575 user = User.find(2)
575 user = User.find(2)
576 member = Member.find(1)
576 member = Member.find(1)
577 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
577 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
578
578
579 assert_equal %w(due_date start_date), issue.required_attribute_names(user).sort
579 assert_equal %w(due_date start_date), issue.required_attribute_names(user).sort
580
580
581 member.role_ids = [1, 2]
581 member.role_ids = [1, 2]
582 member.save!
582 member.save!
583 assert_equal [], issue.required_attribute_names(user.reload)
583 assert_equal [], issue.required_attribute_names(user.reload)
584
584
585 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 2, :field_name => 'due_date', :rule => 'required')
585 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 2, :field_name => 'due_date', :rule => 'required')
586 assert_equal %w(due_date), issue.required_attribute_names(user)
586 assert_equal %w(due_date), issue.required_attribute_names(user)
587
587
588 member.role_ids = [1, 2, 3]
588 member.role_ids = [1, 2, 3]
589 member.save!
589 member.save!
590 assert_equal [], issue.required_attribute_names(user.reload)
590 assert_equal [], issue.required_attribute_names(user.reload)
591
591
592 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 2, :field_name => 'due_date', :rule => 'readonly')
592 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 2, :field_name => 'due_date', :rule => 'readonly')
593 # required + readonly => required
593 # required + readonly => required
594 assert_equal %w(due_date), issue.required_attribute_names(user)
594 assert_equal %w(due_date), issue.required_attribute_names(user)
595 end
595 end
596
596
597 def test_read_only_attribute_names_for_multiple_roles_should_intersect_rules
597 def test_read_only_attribute_names_for_multiple_roles_should_intersect_rules
598 WorkflowPermission.delete_all
598 WorkflowPermission.delete_all
599 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => 'due_date', :rule => 'readonly')
599 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => 'due_date', :rule => 'readonly')
600 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => 'start_date', :rule => 'readonly')
600 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => 'start_date', :rule => 'readonly')
601 user = User.find(2)
601 user = User.find(2)
602 member = Member.find(1)
602 member = Member.find(1)
603 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
603 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
604
604
605 assert_equal %w(due_date start_date), issue.read_only_attribute_names(user).sort
605 assert_equal %w(due_date start_date), issue.read_only_attribute_names(user).sort
606
606
607 member.role_ids = [1, 2]
607 member.role_ids = [1, 2]
608 member.save!
608 member.save!
609 assert_equal [], issue.read_only_attribute_names(user.reload)
609 assert_equal [], issue.read_only_attribute_names(user.reload)
610
610
611 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 2, :field_name => 'due_date', :rule => 'readonly')
611 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 2, :field_name => 'due_date', :rule => 'readonly')
612 assert_equal %w(due_date), issue.read_only_attribute_names(user)
612 assert_equal %w(due_date), issue.read_only_attribute_names(user)
613 end
613 end
614
614
615 def test_copy
615 def test_copy
616 issue = Issue.new.copy_from(1)
616 issue = Issue.new.copy_from(1)
617 assert issue.copy?
617 assert issue.copy?
618 assert issue.save
618 assert issue.save
619 issue.reload
619 issue.reload
620 orig = Issue.find(1)
620 orig = Issue.find(1)
621 assert_equal orig.subject, issue.subject
621 assert_equal orig.subject, issue.subject
622 assert_equal orig.tracker, issue.tracker
622 assert_equal orig.tracker, issue.tracker
623 assert_equal "125", issue.custom_value_for(2).value
623 assert_equal "125", issue.custom_value_for(2).value
624 end
624 end
625
625
626 def test_copy_should_copy_status
626 def test_copy_should_copy_status
627 orig = Issue.find(8)
627 orig = Issue.find(8)
628 assert orig.status != IssueStatus.default
628 assert orig.status != IssueStatus.default
629
629
630 issue = Issue.new.copy_from(orig)
630 issue = Issue.new.copy_from(orig)
631 assert issue.save
631 assert issue.save
632 issue.reload
632 issue.reload
633 assert_equal orig.status, issue.status
633 assert_equal orig.status, issue.status
634 end
634 end
635
635
636 def test_should_not_call_after_project_change_on_creation
636 def test_should_not_call_after_project_change_on_creation
637 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1, :subject => 'Test', :author_id => 1)
637 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1, :subject => 'Test', :author_id => 1)
638 issue.expects(:after_project_change).never
638 issue.expects(:after_project_change).never
639 issue.save!
639 issue.save!
640 end
640 end
641
641
642 def test_should_not_call_after_project_change_on_update
642 def test_should_not_call_after_project_change_on_update
643 issue = Issue.find(1)
643 issue = Issue.find(1)
644 issue.project = Project.find(1)
644 issue.project = Project.find(1)
645 issue.subject = 'No project change'
645 issue.subject = 'No project change'
646 issue.expects(:after_project_change).never
646 issue.expects(:after_project_change).never
647 issue.save!
647 issue.save!
648 end
648 end
649
649
650 def test_should_call_after_project_change_on_project_change
650 def test_should_call_after_project_change_on_project_change
651 issue = Issue.find(1)
651 issue = Issue.find(1)
652 issue.project = Project.find(2)
652 issue.project = Project.find(2)
653 issue.expects(:after_project_change).once
653 issue.expects(:after_project_change).once
654 issue.save!
654 issue.save!
655 end
655 end
656
656
657 def test_adding_journal_should_update_timestamp
657 def test_adding_journal_should_update_timestamp
658 issue = Issue.find(1)
658 issue = Issue.find(1)
659 updated_on_was = issue.updated_on
659 updated_on_was = issue.updated_on
660
660
661 issue.init_journal(User.first, "Adding notes")
661 issue.init_journal(User.first, "Adding notes")
662 assert_difference 'Journal.count' do
662 assert_difference 'Journal.count' do
663 assert issue.save
663 assert issue.save
664 end
664 end
665 issue.reload
665 issue.reload
666
666
667 assert_not_equal updated_on_was, issue.updated_on
667 assert_not_equal updated_on_was, issue.updated_on
668 end
668 end
669
669
670 def test_should_close_duplicates
670 def test_should_close_duplicates
671 # Create 3 issues
671 # Create 3 issues
672 project = Project.find(1)
672 project = Project.find(1)
673 issue1 = Issue.generate_for_project!(project)
673 issue1 = Issue.generate_for_project!(project)
674 issue2 = Issue.generate_for_project!(project)
674 issue2 = Issue.generate_for_project!(project)
675 issue3 = Issue.generate_for_project!(project)
675 issue3 = Issue.generate_for_project!(project)
676
676
677 # 2 is a dupe of 1
677 # 2 is a dupe of 1
678 IssueRelation.create!(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
678 IssueRelation.create!(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
679 # And 3 is a dupe of 2
679 # And 3 is a dupe of 2
680 IssueRelation.create!(:issue_from => issue3, :issue_to => issue2, :relation_type => IssueRelation::TYPE_DUPLICATES)
680 IssueRelation.create!(:issue_from => issue3, :issue_to => issue2, :relation_type => IssueRelation::TYPE_DUPLICATES)
681 # And 3 is a dupe of 1 (circular duplicates)
681 # And 3 is a dupe of 1 (circular duplicates)
682 IssueRelation.create!(:issue_from => issue3, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
682 IssueRelation.create!(:issue_from => issue3, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
683
683
684 assert issue1.reload.duplicates.include?(issue2)
684 assert issue1.reload.duplicates.include?(issue2)
685
685
686 # Closing issue 1
686 # Closing issue 1
687 issue1.init_journal(User.find(:first), "Closing issue1")
687 issue1.init_journal(User.find(:first), "Closing issue1")
688 issue1.status = IssueStatus.find :first, :conditions => {:is_closed => true}
688 issue1.status = IssueStatus.find :first, :conditions => {:is_closed => true}
689 assert issue1.save
689 assert issue1.save
690 # 2 and 3 should be also closed
690 # 2 and 3 should be also closed
691 assert issue2.reload.closed?
691 assert issue2.reload.closed?
692 assert issue3.reload.closed?
692 assert issue3.reload.closed?
693 end
693 end
694
694
695 def test_should_not_close_duplicated_issue
695 def test_should_not_close_duplicated_issue
696 project = Project.find(1)
696 project = Project.find(1)
697 issue1 = Issue.generate_for_project!(project)
697 issue1 = Issue.generate_for_project!(project)
698 issue2 = Issue.generate_for_project!(project)
698 issue2 = Issue.generate_for_project!(project)
699
699
700 # 2 is a dupe of 1
700 # 2 is a dupe of 1
701 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
701 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
702 # 2 is a dup of 1 but 1 is not a duplicate of 2
702 # 2 is a dup of 1 but 1 is not a duplicate of 2
703 assert !issue2.reload.duplicates.include?(issue1)
703 assert !issue2.reload.duplicates.include?(issue1)
704
704
705 # Closing issue 2
705 # Closing issue 2
706 issue2.init_journal(User.find(:first), "Closing issue2")
706 issue2.init_journal(User.find(:first), "Closing issue2")
707 issue2.status = IssueStatus.find :first, :conditions => {:is_closed => true}
707 issue2.status = IssueStatus.find :first, :conditions => {:is_closed => true}
708 assert issue2.save
708 assert issue2.save
709 # 1 should not be also closed
709 # 1 should not be also closed
710 assert !issue1.reload.closed?
710 assert !issue1.reload.closed?
711 end
711 end
712
712
713 def test_assignable_versions
713 def test_assignable_versions
714 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
714 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
715 assert_equal ['open'], issue.assignable_versions.collect(&:status).uniq
715 assert_equal ['open'], issue.assignable_versions.collect(&:status).uniq
716 end
716 end
717
717
718 def test_should_not_be_able_to_assign_a_new_issue_to_a_closed_version
718 def test_should_not_be_able_to_assign_a_new_issue_to_a_closed_version
719 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
719 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
720 assert !issue.save
720 assert !issue.save
721 assert_not_nil issue.errors[:fixed_version_id]
721 assert_not_nil issue.errors[:fixed_version_id]
722 end
722 end
723
723
724 def test_should_not_be_able_to_assign_a_new_issue_to_a_locked_version
724 def test_should_not_be_able_to_assign_a_new_issue_to_a_locked_version
725 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 2, :subject => 'New issue')
725 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 2, :subject => 'New issue')
726 assert !issue.save
726 assert !issue.save
727 assert_not_nil issue.errors[:fixed_version_id]
727 assert_not_nil issue.errors[:fixed_version_id]
728 end
728 end
729
729
730 def test_should_be_able_to_assign_a_new_issue_to_an_open_version
730 def test_should_be_able_to_assign_a_new_issue_to_an_open_version
731 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 3, :subject => 'New issue')
731 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 3, :subject => 'New issue')
732 assert issue.save
732 assert issue.save
733 end
733 end
734
734
735 def test_should_be_able_to_update_an_issue_assigned_to_a_closed_version
735 def test_should_be_able_to_update_an_issue_assigned_to_a_closed_version
736 issue = Issue.find(11)
736 issue = Issue.find(11)
737 assert_equal 'closed', issue.fixed_version.status
737 assert_equal 'closed', issue.fixed_version.status
738 issue.subject = 'Subject changed'
738 issue.subject = 'Subject changed'
739 assert issue.save
739 assert issue.save
740 end
740 end
741
741
742 def test_should_not_be_able_to_reopen_an_issue_assigned_to_a_closed_version
742 def test_should_not_be_able_to_reopen_an_issue_assigned_to_a_closed_version
743 issue = Issue.find(11)
743 issue = Issue.find(11)
744 issue.status_id = 1
744 issue.status_id = 1
745 assert !issue.save
745 assert !issue.save
746 assert_not_nil issue.errors[:base]
746 assert_not_nil issue.errors[:base]
747 end
747 end
748
748
749 def test_should_be_able_to_reopen_and_reassign_an_issue_assigned_to_a_closed_version
749 def test_should_be_able_to_reopen_and_reassign_an_issue_assigned_to_a_closed_version
750 issue = Issue.find(11)
750 issue = Issue.find(11)
751 issue.status_id = 1
751 issue.status_id = 1
752 issue.fixed_version_id = 3
752 issue.fixed_version_id = 3
753 assert issue.save
753 assert issue.save
754 end
754 end
755
755
756 def test_should_be_able_to_reopen_an_issue_assigned_to_a_locked_version
756 def test_should_be_able_to_reopen_an_issue_assigned_to_a_locked_version
757 issue = Issue.find(12)
757 issue = Issue.find(12)
758 assert_equal 'locked', issue.fixed_version.status
758 assert_equal 'locked', issue.fixed_version.status
759 issue.status_id = 1
759 issue.status_id = 1
760 assert issue.save
760 assert issue.save
761 end
761 end
762
762
763 def test_should_not_be_able_to_keep_unshared_version_when_changing_project
763 def test_should_not_be_able_to_keep_unshared_version_when_changing_project
764 issue = Issue.find(2)
764 issue = Issue.find(2)
765 assert_equal 2, issue.fixed_version_id
765 assert_equal 2, issue.fixed_version_id
766 issue.project_id = 3
766 issue.project_id = 3
767 assert_nil issue.fixed_version_id
767 assert_nil issue.fixed_version_id
768 issue.fixed_version_id = 2
768 issue.fixed_version_id = 2
769 assert !issue.save
769 assert !issue.save
770 assert_include 'Target version is not included in the list', issue.errors.full_messages
770 assert_include 'Target version is not included in the list', issue.errors.full_messages
771 end
771 end
772
772
773 def test_should_keep_shared_version_when_changing_project
773 def test_should_keep_shared_version_when_changing_project
774 Version.find(2).update_attribute :sharing, 'tree'
774 Version.find(2).update_attribute :sharing, 'tree'
775
775
776 issue = Issue.find(2)
776 issue = Issue.find(2)
777 assert_equal 2, issue.fixed_version_id
777 assert_equal 2, issue.fixed_version_id
778 issue.project_id = 3
778 issue.project_id = 3
779 assert_equal 2, issue.fixed_version_id
779 assert_equal 2, issue.fixed_version_id
780 assert issue.save
780 assert issue.save
781 end
781 end
782
782
783 def test_allowed_target_projects_on_move_should_include_projects_with_issue_tracking_enabled
783 def test_allowed_target_projects_on_move_should_include_projects_with_issue_tracking_enabled
784 assert_include Project.find(2), Issue.allowed_target_projects_on_move(User.find(2))
784 assert_include Project.find(2), Issue.allowed_target_projects_on_move(User.find(2))
785 end
785 end
786
786
787 def test_allowed_target_projects_on_move_should_not_include_projects_with_issue_tracking_disabled
787 def test_allowed_target_projects_on_move_should_not_include_projects_with_issue_tracking_disabled
788 Project.find(2).disable_module! :issue_tracking
788 Project.find(2).disable_module! :issue_tracking
789 assert_not_include Project.find(2), Issue.allowed_target_projects_on_move(User.find(2))
789 assert_not_include Project.find(2), Issue.allowed_target_projects_on_move(User.find(2))
790 end
790 end
791
791
792 def test_move_to_another_project_with_same_category
792 def test_move_to_another_project_with_same_category
793 issue = Issue.find(1)
793 issue = Issue.find(1)
794 issue.project = Project.find(2)
794 issue.project = Project.find(2)
795 assert issue.save
795 assert issue.save
796 issue.reload
796 issue.reload
797 assert_equal 2, issue.project_id
797 assert_equal 2, issue.project_id
798 # Category changes
798 # Category changes
799 assert_equal 4, issue.category_id
799 assert_equal 4, issue.category_id
800 # Make sure time entries were move to the target project
800 # Make sure time entries were move to the target project
801 assert_equal 2, issue.time_entries.first.project_id
801 assert_equal 2, issue.time_entries.first.project_id
802 end
802 end
803
803
804 def test_move_to_another_project_without_same_category
804 def test_move_to_another_project_without_same_category
805 issue = Issue.find(2)
805 issue = Issue.find(2)
806 issue.project = Project.find(2)
806 issue.project = Project.find(2)
807 assert issue.save
807 assert issue.save
808 issue.reload
808 issue.reload
809 assert_equal 2, issue.project_id
809 assert_equal 2, issue.project_id
810 # Category cleared
810 # Category cleared
811 assert_nil issue.category_id
811 assert_nil issue.category_id
812 end
812 end
813
813
814 def test_move_to_another_project_should_clear_fixed_version_when_not_shared
814 def test_move_to_another_project_should_clear_fixed_version_when_not_shared
815 issue = Issue.find(1)
815 issue = Issue.find(1)
816 issue.update_attribute(:fixed_version_id, 1)
816 issue.update_attribute(:fixed_version_id, 1)
817 issue.project = Project.find(2)
817 issue.project = Project.find(2)
818 assert issue.save
818 assert issue.save
819 issue.reload
819 issue.reload
820 assert_equal 2, issue.project_id
820 assert_equal 2, issue.project_id
821 # Cleared fixed_version
821 # Cleared fixed_version
822 assert_equal nil, issue.fixed_version
822 assert_equal nil, issue.fixed_version
823 end
823 end
824
824
825 def test_move_to_another_project_should_keep_fixed_version_when_shared_with_the_target_project
825 def test_move_to_another_project_should_keep_fixed_version_when_shared_with_the_target_project
826 issue = Issue.find(1)
826 issue = Issue.find(1)
827 issue.update_attribute(:fixed_version_id, 4)
827 issue.update_attribute(:fixed_version_id, 4)
828 issue.project = Project.find(5)
828 issue.project = Project.find(5)
829 assert issue.save
829 assert issue.save
830 issue.reload
830 issue.reload
831 assert_equal 5, issue.project_id
831 assert_equal 5, issue.project_id
832 # Keep fixed_version
832 # Keep fixed_version
833 assert_equal 4, issue.fixed_version_id
833 assert_equal 4, issue.fixed_version_id
834 end
834 end
835
835
836 def test_move_to_another_project_should_clear_fixed_version_when_not_shared_with_the_target_project
836 def test_move_to_another_project_should_clear_fixed_version_when_not_shared_with_the_target_project
837 issue = Issue.find(1)
837 issue = Issue.find(1)
838 issue.update_attribute(:fixed_version_id, 1)
838 issue.update_attribute(:fixed_version_id, 1)
839 issue.project = Project.find(5)
839 issue.project = Project.find(5)
840 assert issue.save
840 assert issue.save
841 issue.reload
841 issue.reload
842 assert_equal 5, issue.project_id
842 assert_equal 5, issue.project_id
843 # Cleared fixed_version
843 # Cleared fixed_version
844 assert_equal nil, issue.fixed_version
844 assert_equal nil, issue.fixed_version
845 end
845 end
846
846
847 def test_move_to_another_project_should_keep_fixed_version_when_shared_systemwide
847 def test_move_to_another_project_should_keep_fixed_version_when_shared_systemwide
848 issue = Issue.find(1)
848 issue = Issue.find(1)
849 issue.update_attribute(:fixed_version_id, 7)
849 issue.update_attribute(:fixed_version_id, 7)
850 issue.project = Project.find(2)
850 issue.project = Project.find(2)
851 assert issue.save
851 assert issue.save
852 issue.reload
852 issue.reload
853 assert_equal 2, issue.project_id
853 assert_equal 2, issue.project_id
854 # Keep fixed_version
854 # Keep fixed_version
855 assert_equal 7, issue.fixed_version_id
855 assert_equal 7, issue.fixed_version_id
856 end
856 end
857
857
858 def test_move_to_another_project_with_disabled_tracker
858 def test_move_to_another_project_with_disabled_tracker
859 issue = Issue.find(1)
859 issue = Issue.find(1)
860 target = Project.find(2)
860 target = Project.find(2)
861 target.tracker_ids = [3]
861 target.tracker_ids = [3]
862 target.save
862 target.save
863 issue.project = target
863 issue.project = target
864 assert issue.save
864 assert issue.save
865 issue.reload
865 issue.reload
866 assert_equal 2, issue.project_id
866 assert_equal 2, issue.project_id
867 assert_equal 3, issue.tracker_id
867 assert_equal 3, issue.tracker_id
868 end
868 end
869
869
870 def test_copy_to_the_same_project
870 def test_copy_to_the_same_project
871 issue = Issue.find(1)
871 issue = Issue.find(1)
872 copy = issue.copy
872 copy = issue.copy
873 assert_difference 'Issue.count' do
873 assert_difference 'Issue.count' do
874 copy.save!
874 copy.save!
875 end
875 end
876 assert_kind_of Issue, copy
876 assert_kind_of Issue, copy
877 assert_equal issue.project, copy.project
877 assert_equal issue.project, copy.project
878 assert_equal "125", copy.custom_value_for(2).value
878 assert_equal "125", copy.custom_value_for(2).value
879 end
879 end
880
880
881 def test_copy_to_another_project_and_tracker
881 def test_copy_to_another_project_and_tracker
882 issue = Issue.find(1)
882 issue = Issue.find(1)
883 copy = issue.copy(:project_id => 3, :tracker_id => 2)
883 copy = issue.copy(:project_id => 3, :tracker_id => 2)
884 assert_difference 'Issue.count' do
884 assert_difference 'Issue.count' do
885 copy.save!
885 copy.save!
886 end
886 end
887 copy.reload
887 copy.reload
888 assert_kind_of Issue, copy
888 assert_kind_of Issue, copy
889 assert_equal Project.find(3), copy.project
889 assert_equal Project.find(3), copy.project
890 assert_equal Tracker.find(2), copy.tracker
890 assert_equal Tracker.find(2), copy.tracker
891 # Custom field #2 is not associated with target tracker
891 # Custom field #2 is not associated with target tracker
892 assert_nil copy.custom_value_for(2)
892 assert_nil copy.custom_value_for(2)
893 end
893 end
894
894
895 context "#copy" do
895 context "#copy" do
896 setup do
896 setup do
897 @issue = Issue.find(1)
897 @issue = Issue.find(1)
898 end
898 end
899
899
900 should "not create a journal" do
900 should "not create a journal" do
901 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :assigned_to_id => 3)
901 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :assigned_to_id => 3)
902 copy.save!
902 copy.save!
903 assert_equal 0, copy.reload.journals.size
903 assert_equal 0, copy.reload.journals.size
904 end
904 end
905
905
906 should "allow assigned_to changes" do
906 should "allow assigned_to changes" do
907 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :assigned_to_id => 3)
907 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :assigned_to_id => 3)
908 assert_equal 3, copy.assigned_to_id
908 assert_equal 3, copy.assigned_to_id
909 end
909 end
910
910
911 should "allow status changes" do
911 should "allow status changes" do
912 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :status_id => 2)
912 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :status_id => 2)
913 assert_equal 2, copy.status_id
913 assert_equal 2, copy.status_id
914 end
914 end
915
915
916 should "allow start date changes" do
916 should "allow start date changes" do
917 date = Date.today
917 date = Date.today
918 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :start_date => date)
918 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :start_date => date)
919 assert_equal date, copy.start_date
919 assert_equal date, copy.start_date
920 end
920 end
921
921
922 should "allow due date changes" do
922 should "allow due date changes" do
923 date = Date.today
923 date = Date.today
924 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :due_date => date)
924 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :due_date => date)
925 assert_equal date, copy.due_date
925 assert_equal date, copy.due_date
926 end
926 end
927
927
928 should "set current user as author" do
928 should "set current user as author" do
929 User.current = User.find(9)
929 User.current = User.find(9)
930 copy = @issue.copy(:project_id => 3, :tracker_id => 2)
930 copy = @issue.copy(:project_id => 3, :tracker_id => 2)
931 assert_equal User.current, copy.author
931 assert_equal User.current, copy.author
932 end
932 end
933
933
934 should "create a journal with notes" do
934 should "create a journal with notes" do
935 date = Date.today
935 date = Date.today
936 notes = "Notes added when copying"
936 notes = "Notes added when copying"
937 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :start_date => date)
937 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :start_date => date)
938 copy.init_journal(User.current, notes)
938 copy.init_journal(User.current, notes)
939 copy.save!
939 copy.save!
940
940
941 assert_equal 1, copy.journals.size
941 assert_equal 1, copy.journals.size
942 journal = copy.journals.first
942 journal = copy.journals.first
943 assert_equal 0, journal.details.size
943 assert_equal 0, journal.details.size
944 assert_equal notes, journal.notes
944 assert_equal notes, journal.notes
945 end
945 end
946 end
946 end
947
947
948 def test_recipients_should_include_previous_assignee
948 def test_recipients_should_include_previous_assignee
949 user = User.find(3)
949 user = User.find(3)
950 user.members.update_all ["mail_notification = ?", false]
950 user.members.update_all ["mail_notification = ?", false]
951 user.update_attribute :mail_notification, 'only_assigned'
951 user.update_attribute :mail_notification, 'only_assigned'
952
952
953 issue = Issue.find(2)
953 issue = Issue.find(2)
954 issue.assigned_to = nil
954 issue.assigned_to = nil
955 assert_include user.mail, issue.recipients
955 assert_include user.mail, issue.recipients
956 issue.save!
956 issue.save!
957 assert !issue.recipients.include?(user.mail)
957 assert !issue.recipients.include?(user.mail)
958 end
958 end
959
959
960 def test_recipients_should_not_include_users_that_cannot_view_the_issue
960 def test_recipients_should_not_include_users_that_cannot_view_the_issue
961 issue = Issue.find(12)
961 issue = Issue.find(12)
962 assert issue.recipients.include?(issue.author.mail)
962 assert issue.recipients.include?(issue.author.mail)
963 # copy the issue to a private project
963 # copy the issue to a private project
964 copy = issue.copy(:project_id => 5, :tracker_id => 2)
964 copy = issue.copy(:project_id => 5, :tracker_id => 2)
965 # author is not a member of project anymore
965 # author is not a member of project anymore
966 assert !copy.recipients.include?(copy.author.mail)
966 assert !copy.recipients.include?(copy.author.mail)
967 end
967 end
968
968
969 def test_recipients_should_include_the_assigned_group_members
969 def test_recipients_should_include_the_assigned_group_members
970 group_member = User.generate!
970 group_member = User.generate!
971 group = Group.generate!
971 group = Group.generate!
972 group.users << group_member
972 group.users << group_member
973
973
974 issue = Issue.find(12)
974 issue = Issue.find(12)
975 issue.assigned_to = group
975 issue.assigned_to = group
976 assert issue.recipients.include?(group_member.mail)
976 assert issue.recipients.include?(group_member.mail)
977 end
977 end
978
978
979 def test_watcher_recipients_should_not_include_users_that_cannot_view_the_issue
979 def test_watcher_recipients_should_not_include_users_that_cannot_view_the_issue
980 user = User.find(3)
980 user = User.find(3)
981 issue = Issue.find(9)
981 issue = Issue.find(9)
982 Watcher.create!(:user => user, :watchable => issue)
982 Watcher.create!(:user => user, :watchable => issue)
983 assert issue.watched_by?(user)
983 assert issue.watched_by?(user)
984 assert !issue.watcher_recipients.include?(user.mail)
984 assert !issue.watcher_recipients.include?(user.mail)
985 end
985 end
986
986
987 def test_issue_destroy
987 def test_issue_destroy
988 Issue.find(1).destroy
988 Issue.find(1).destroy
989 assert_nil Issue.find_by_id(1)
989 assert_nil Issue.find_by_id(1)
990 assert_nil TimeEntry.find_by_issue_id(1)
990 assert_nil TimeEntry.find_by_issue_id(1)
991 end
991 end
992
992
993 def test_destroying_a_deleted_issue_should_not_raise_an_error
993 def test_destroying_a_deleted_issue_should_not_raise_an_error
994 issue = Issue.find(1)
994 issue = Issue.find(1)
995 Issue.find(1).destroy
995 Issue.find(1).destroy
996
996
997 assert_nothing_raised do
997 assert_nothing_raised do
998 assert_no_difference 'Issue.count' do
998 assert_no_difference 'Issue.count' do
999 issue.destroy
999 issue.destroy
1000 end
1000 end
1001 assert issue.destroyed?
1001 assert issue.destroyed?
1002 end
1002 end
1003 end
1003 end
1004
1004
1005 def test_destroying_a_stale_issue_should_not_raise_an_error
1005 def test_destroying_a_stale_issue_should_not_raise_an_error
1006 issue = Issue.find(1)
1006 issue = Issue.find(1)
1007 Issue.find(1).update_attribute :subject, "Updated"
1007 Issue.find(1).update_attribute :subject, "Updated"
1008
1008
1009 assert_nothing_raised do
1009 assert_nothing_raised do
1010 assert_difference 'Issue.count', -1 do
1010 assert_difference 'Issue.count', -1 do
1011 issue.destroy
1011 issue.destroy
1012 end
1012 end
1013 assert issue.destroyed?
1013 assert issue.destroyed?
1014 end
1014 end
1015 end
1015 end
1016
1016
1017 def test_blocked
1017 def test_blocked
1018 blocked_issue = Issue.find(9)
1018 blocked_issue = Issue.find(9)
1019 blocking_issue = Issue.find(10)
1019 blocking_issue = Issue.find(10)
1020
1020
1021 assert blocked_issue.blocked?
1021 assert blocked_issue.blocked?
1022 assert !blocking_issue.blocked?
1022 assert !blocking_issue.blocked?
1023 end
1023 end
1024
1024
1025 def test_blocked_issues_dont_allow_closed_statuses
1025 def test_blocked_issues_dont_allow_closed_statuses
1026 blocked_issue = Issue.find(9)
1026 blocked_issue = Issue.find(9)
1027
1027
1028 allowed_statuses = blocked_issue.new_statuses_allowed_to(users(:users_002))
1028 allowed_statuses = blocked_issue.new_statuses_allowed_to(users(:users_002))
1029 assert !allowed_statuses.empty?
1029 assert !allowed_statuses.empty?
1030 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
1030 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
1031 assert closed_statuses.empty?
1031 assert closed_statuses.empty?
1032 end
1032 end
1033
1033
1034 def test_unblocked_issues_allow_closed_statuses
1034 def test_unblocked_issues_allow_closed_statuses
1035 blocking_issue = Issue.find(10)
1035 blocking_issue = Issue.find(10)
1036
1036
1037 allowed_statuses = blocking_issue.new_statuses_allowed_to(users(:users_002))
1037 allowed_statuses = blocking_issue.new_statuses_allowed_to(users(:users_002))
1038 assert !allowed_statuses.empty?
1038 assert !allowed_statuses.empty?
1039 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
1039 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
1040 assert !closed_statuses.empty?
1040 assert !closed_statuses.empty?
1041 end
1041 end
1042
1042
1043 def test_rescheduling_an_issue_should_reschedule_following_issue
1043 def test_rescheduling_an_issue_should_reschedule_following_issue
1044 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)
1044 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)
1045 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)
1045 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)
1046 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_PRECEDES)
1046 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_PRECEDES)
1047 assert_equal issue1.due_date + 1, issue2.reload.start_date
1047 assert_equal issue1.due_date + 1, issue2.reload.start_date
1048
1048
1049 issue1.due_date = Date.today + 5
1049 issue1.due_date = Date.today + 5
1050 issue1.save!
1050 issue1.save!
1051 assert_equal issue1.due_date + 1, issue2.reload.start_date
1051 assert_equal issue1.due_date + 1, issue2.reload.start_date
1052 end
1052 end
1053
1053
1054 def test_rescheduling_a_stale_issue_should_not_raise_an_error
1054 def test_rescheduling_a_stale_issue_should_not_raise_an_error
1055 stale = Issue.find(1)
1055 stale = Issue.find(1)
1056 issue = Issue.find(1)
1056 issue = Issue.find(1)
1057 issue.subject = "Updated"
1057 issue.subject = "Updated"
1058 issue.save!
1058 issue.save!
1059
1059
1060 date = 10.days.from_now.to_date
1060 date = 10.days.from_now.to_date
1061 assert_nothing_raised do
1061 assert_nothing_raised do
1062 stale.reschedule_after(date)
1062 stale.reschedule_after(date)
1063 end
1063 end
1064 assert_equal date, stale.reload.start_date
1064 assert_equal date, stale.reload.start_date
1065 end
1065 end
1066
1066
1067 def test_overdue
1067 def test_overdue
1068 assert Issue.new(:due_date => 1.day.ago.to_date).overdue?
1068 assert Issue.new(:due_date => 1.day.ago.to_date).overdue?
1069 assert !Issue.new(:due_date => Date.today).overdue?
1069 assert !Issue.new(:due_date => Date.today).overdue?
1070 assert !Issue.new(:due_date => 1.day.from_now.to_date).overdue?
1070 assert !Issue.new(:due_date => 1.day.from_now.to_date).overdue?
1071 assert !Issue.new(:due_date => nil).overdue?
1071 assert !Issue.new(:due_date => nil).overdue?
1072 assert !Issue.new(:due_date => 1.day.ago.to_date, :status => IssueStatus.find(:first, :conditions => {:is_closed => true})).overdue?
1072 assert !Issue.new(:due_date => 1.day.ago.to_date, :status => IssueStatus.find(:first, :conditions => {:is_closed => true})).overdue?
1073 end
1073 end
1074
1074
1075 context "#behind_schedule?" do
1075 context "#behind_schedule?" do
1076 should "be false if the issue has no start_date" do
1076 should "be false if the issue has no start_date" do
1077 assert !Issue.new(:start_date => nil, :due_date => 1.day.from_now.to_date, :done_ratio => 0).behind_schedule?
1077 assert !Issue.new(:start_date => nil, :due_date => 1.day.from_now.to_date, :done_ratio => 0).behind_schedule?
1078 end
1078 end
1079
1079
1080 should "be false if the issue has no end_date" do
1080 should "be false if the issue has no end_date" do
1081 assert !Issue.new(:start_date => 1.day.from_now.to_date, :due_date => nil, :done_ratio => 0).behind_schedule?
1081 assert !Issue.new(:start_date => 1.day.from_now.to_date, :due_date => nil, :done_ratio => 0).behind_schedule?
1082 end
1082 end
1083
1083
1084 should "be false if the issue has more done than it's calendar time" do
1084 should "be false if the issue has more done than it's calendar time" do
1085 assert !Issue.new(:start_date => 50.days.ago.to_date, :due_date => 50.days.from_now.to_date, :done_ratio => 90).behind_schedule?
1085 assert !Issue.new(:start_date => 50.days.ago.to_date, :due_date => 50.days.from_now.to_date, :done_ratio => 90).behind_schedule?
1086 end
1086 end
1087
1087
1088 should "be true if the issue hasn't been started at all" do
1088 should "be true if the issue hasn't been started at all" do
1089 assert Issue.new(:start_date => 1.day.ago.to_date, :due_date => 1.day.from_now.to_date, :done_ratio => 0).behind_schedule?
1089 assert Issue.new(:start_date => 1.day.ago.to_date, :due_date => 1.day.from_now.to_date, :done_ratio => 0).behind_schedule?
1090 end
1090 end
1091
1091
1092 should "be true if the issue has used more calendar time than it's done ratio" do
1092 should "be true if the issue has used more calendar time than it's done ratio" do
1093 assert Issue.new(:start_date => 100.days.ago.to_date, :due_date => Date.today, :done_ratio => 90).behind_schedule?
1093 assert Issue.new(:start_date => 100.days.ago.to_date, :due_date => Date.today, :done_ratio => 90).behind_schedule?
1094 end
1094 end
1095 end
1095 end
1096
1096
1097 context "#assignable_users" do
1097 context "#assignable_users" do
1098 should "be Users" do
1098 should "be Users" do
1099 assert_kind_of User, Issue.find(1).assignable_users.first
1099 assert_kind_of User, Issue.find(1).assignable_users.first
1100 end
1100 end
1101
1101
1102 should "include the issue author" do
1102 should "include the issue author" do
1103 project = Project.find(1)
1103 project = Project.find(1)
1104 non_project_member = User.generate!
1104 non_project_member = User.generate!
1105 issue = Issue.generate_for_project!(project, :author => non_project_member)
1105 issue = Issue.generate_for_project!(project, :author => non_project_member)
1106
1106
1107 assert issue.assignable_users.include?(non_project_member)
1107 assert issue.assignable_users.include?(non_project_member)
1108 end
1108 end
1109
1109
1110 should "include the current assignee" do
1110 should "include the current assignee" do
1111 project = Project.find(1)
1111 project = Project.find(1)
1112 user = User.generate!
1112 user = User.generate!
1113 issue = Issue.generate_for_project!(project, :assigned_to => user)
1113 issue = Issue.generate_for_project!(project, :assigned_to => user)
1114 user.lock!
1114 user.lock!
1115
1115
1116 assert Issue.find(issue.id).assignable_users.include?(user)
1116 assert Issue.find(issue.id).assignable_users.include?(user)
1117 end
1117 end
1118
1118
1119 should "not show the issue author twice" do
1119 should "not show the issue author twice" do
1120 assignable_user_ids = Issue.find(1).assignable_users.collect(&:id)
1120 assignable_user_ids = Issue.find(1).assignable_users.collect(&:id)
1121 assert_equal 2, assignable_user_ids.length
1121 assert_equal 2, assignable_user_ids.length
1122
1122
1123 assignable_user_ids.each do |user_id|
1123 assignable_user_ids.each do |user_id|
1124 assert_equal 1, assignable_user_ids.select {|i| i == user_id}.length, "User #{user_id} appears more or less than once"
1124 assert_equal 1, assignable_user_ids.select {|i| i == user_id}.length, "User #{user_id} appears more or less than once"
1125 end
1125 end
1126 end
1126 end
1127
1127
1128 context "with issue_group_assignment" do
1128 context "with issue_group_assignment" do
1129 should "include groups" do
1129 should "include groups" do
1130 issue = Issue.new(:project => Project.find(2))
1130 issue = Issue.new(:project => Project.find(2))
1131
1131
1132 with_settings :issue_group_assignment => '1' do
1132 with_settings :issue_group_assignment => '1' do
1133 assert_equal %w(Group User), issue.assignable_users.map {|a| a.class.name}.uniq.sort
1133 assert_equal %w(Group User), issue.assignable_users.map {|a| a.class.name}.uniq.sort
1134 assert issue.assignable_users.include?(Group.find(11))
1134 assert issue.assignable_users.include?(Group.find(11))
1135 end
1135 end
1136 end
1136 end
1137 end
1137 end
1138
1138
1139 context "without issue_group_assignment" do
1139 context "without issue_group_assignment" do
1140 should "not include groups" do
1140 should "not include groups" do
1141 issue = Issue.new(:project => Project.find(2))
1141 issue = Issue.new(:project => Project.find(2))
1142
1142
1143 with_settings :issue_group_assignment => '0' do
1143 with_settings :issue_group_assignment => '0' do
1144 assert_equal %w(User), issue.assignable_users.map {|a| a.class.name}.uniq.sort
1144 assert_equal %w(User), issue.assignable_users.map {|a| a.class.name}.uniq.sort
1145 assert !issue.assignable_users.include?(Group.find(11))
1145 assert !issue.assignable_users.include?(Group.find(11))
1146 end
1146 end
1147 end
1147 end
1148 end
1148 end
1149 end
1149 end
1150
1150
1151 def test_create_should_send_email_notification
1151 def test_create_should_send_email_notification
1152 ActionMailer::Base.deliveries.clear
1152 ActionMailer::Base.deliveries.clear
1153 issue = Issue.new(:project_id => 1, :tracker_id => 1,
1153 issue = Issue.new(:project_id => 1, :tracker_id => 1,
1154 :author_id => 3, :status_id => 1,
1154 :author_id => 3, :status_id => 1,
1155 :priority => IssuePriority.all.first,
1155 :priority => IssuePriority.all.first,
1156 :subject => 'test_create', :estimated_hours => '1:30')
1156 :subject => 'test_create', :estimated_hours => '1:30')
1157
1157
1158 assert issue.save
1158 assert issue.save
1159 assert_equal 1, ActionMailer::Base.deliveries.size
1159 assert_equal 1, ActionMailer::Base.deliveries.size
1160 end
1160 end
1161
1161
1162 def test_stale_issue_should_not_send_email_notification
1162 def test_stale_issue_should_not_send_email_notification
1163 ActionMailer::Base.deliveries.clear
1163 ActionMailer::Base.deliveries.clear
1164 issue = Issue.find(1)
1164 issue = Issue.find(1)
1165 stale = Issue.find(1)
1165 stale = Issue.find(1)
1166
1166
1167 issue.init_journal(User.find(1))
1167 issue.init_journal(User.find(1))
1168 issue.subject = 'Subjet update'
1168 issue.subject = 'Subjet update'
1169 assert issue.save
1169 assert issue.save
1170 assert_equal 1, ActionMailer::Base.deliveries.size
1170 assert_equal 1, ActionMailer::Base.deliveries.size
1171 ActionMailer::Base.deliveries.clear
1171 ActionMailer::Base.deliveries.clear
1172
1172
1173 stale.init_journal(User.find(1))
1173 stale.init_journal(User.find(1))
1174 stale.subject = 'Another subjet update'
1174 stale.subject = 'Another subjet update'
1175 assert_raise ActiveRecord::StaleObjectError do
1175 assert_raise ActiveRecord::StaleObjectError do
1176 stale.save
1176 stale.save
1177 end
1177 end
1178 assert ActionMailer::Base.deliveries.empty?
1178 assert ActionMailer::Base.deliveries.empty?
1179 end
1179 end
1180
1180
1181 def test_journalized_description
1181 def test_journalized_description
1182 IssueCustomField.delete_all
1182 IssueCustomField.delete_all
1183
1183
1184 i = Issue.first
1184 i = Issue.first
1185 old_description = i.description
1185 old_description = i.description
1186 new_description = "This is the new description"
1186 new_description = "This is the new description"
1187
1187
1188 i.init_journal(User.find(2))
1188 i.init_journal(User.find(2))
1189 i.description = new_description
1189 i.description = new_description
1190 assert_difference 'Journal.count', 1 do
1190 assert_difference 'Journal.count', 1 do
1191 assert_difference 'JournalDetail.count', 1 do
1191 assert_difference 'JournalDetail.count', 1 do
1192 i.save!
1192 i.save!
1193 end
1193 end
1194 end
1194 end
1195
1195
1196 detail = JournalDetail.first(:order => 'id DESC')
1196 detail = JournalDetail.first(:order => 'id DESC')
1197 assert_equal i, detail.journal.journalized
1197 assert_equal i, detail.journal.journalized
1198 assert_equal 'attr', detail.property
1198 assert_equal 'attr', detail.property
1199 assert_equal 'description', detail.prop_key
1199 assert_equal 'description', detail.prop_key
1200 assert_equal old_description, detail.old_value
1200 assert_equal old_description, detail.old_value
1201 assert_equal new_description, detail.value
1201 assert_equal new_description, detail.value
1202 end
1202 end
1203
1203
1204 def test_blank_descriptions_should_not_be_journalized
1204 def test_blank_descriptions_should_not_be_journalized
1205 IssueCustomField.delete_all
1205 IssueCustomField.delete_all
1206 Issue.update_all("description = NULL", "id=1")
1206 Issue.update_all("description = NULL", "id=1")
1207
1207
1208 i = Issue.find(1)
1208 i = Issue.find(1)
1209 i.init_journal(User.find(2))
1209 i.init_journal(User.find(2))
1210 i.subject = "blank description"
1210 i.subject = "blank description"
1211 i.description = "\r\n"
1211 i.description = "\r\n"
1212
1212
1213 assert_difference 'Journal.count', 1 do
1213 assert_difference 'Journal.count', 1 do
1214 assert_difference 'JournalDetail.count', 1 do
1214 assert_difference 'JournalDetail.count', 1 do
1215 i.save!
1215 i.save!
1216 end
1216 end
1217 end
1217 end
1218 end
1218 end
1219
1219
1220 def test_journalized_multi_custom_field
1220 def test_journalized_multi_custom_field
1221 field = IssueCustomField.create!(:name => 'filter', :field_format => 'list', :is_filter => true, :is_for_all => true,
1221 field = IssueCustomField.create!(:name => 'filter', :field_format => 'list', :is_filter => true, :is_for_all => true,
1222 :tracker_ids => [1], :possible_values => ['value1', 'value2', 'value3'], :multiple => true)
1222 :tracker_ids => [1], :possible_values => ['value1', 'value2', 'value3'], :multiple => true)
1223
1223
1224 issue = Issue.create!(:project_id => 1, :tracker_id => 1, :subject => 'Test', :author_id => 1)
1224 issue = Issue.create!(:project_id => 1, :tracker_id => 1, :subject => 'Test', :author_id => 1)
1225
1225
1226 assert_difference 'Journal.count' do
1226 assert_difference 'Journal.count' do
1227 assert_difference 'JournalDetail.count' do
1227 assert_difference 'JournalDetail.count' do
1228 issue.init_journal(User.first)
1228 issue.init_journal(User.first)
1229 issue.custom_field_values = {field.id => ['value1']}
1229 issue.custom_field_values = {field.id => ['value1']}
1230 issue.save!
1230 issue.save!
1231 end
1231 end
1232 assert_difference 'JournalDetail.count' do
1232 assert_difference 'JournalDetail.count' do
1233 issue.init_journal(User.first)
1233 issue.init_journal(User.first)
1234 issue.custom_field_values = {field.id => ['value1', 'value2']}
1234 issue.custom_field_values = {field.id => ['value1', 'value2']}
1235 issue.save!
1235 issue.save!
1236 end
1236 end
1237 assert_difference 'JournalDetail.count', 2 do
1237 assert_difference 'JournalDetail.count', 2 do
1238 issue.init_journal(User.first)
1238 issue.init_journal(User.first)
1239 issue.custom_field_values = {field.id => ['value3', 'value2']}
1239 issue.custom_field_values = {field.id => ['value3', 'value2']}
1240 issue.save!
1240 issue.save!
1241 end
1241 end
1242 assert_difference 'JournalDetail.count', 2 do
1242 assert_difference 'JournalDetail.count', 2 do
1243 issue.init_journal(User.first)
1243 issue.init_journal(User.first)
1244 issue.custom_field_values = {field.id => nil}
1244 issue.custom_field_values = {field.id => nil}
1245 issue.save!
1245 issue.save!
1246 end
1246 end
1247 end
1247 end
1248 end
1248 end
1249
1249
1250 def test_description_eol_should_be_normalized
1250 def test_description_eol_should_be_normalized
1251 i = Issue.new(:description => "CR \r LF \n CRLF \r\n")
1251 i = Issue.new(:description => "CR \r LF \n CRLF \r\n")
1252 assert_equal "CR \r\n LF \r\n CRLF \r\n", i.description
1252 assert_equal "CR \r\n LF \r\n CRLF \r\n", i.description
1253 end
1253 end
1254
1254
1255 def test_saving_twice_should_not_duplicate_journal_details
1255 def test_saving_twice_should_not_duplicate_journal_details
1256 i = Issue.find(:first)
1256 i = Issue.find(:first)
1257 i.init_journal(User.find(2), 'Some notes')
1257 i.init_journal(User.find(2), 'Some notes')
1258 # initial changes
1258 # initial changes
1259 i.subject = 'New subject'
1259 i.subject = 'New subject'
1260 i.done_ratio = i.done_ratio + 10
1260 i.done_ratio = i.done_ratio + 10
1261 assert_difference 'Journal.count' do
1261 assert_difference 'Journal.count' do
1262 assert i.save
1262 assert i.save
1263 end
1263 end
1264 # 1 more change
1264 # 1 more change
1265 i.priority = IssuePriority.find(:first, :conditions => ["id <> ?", i.priority_id])
1265 i.priority = IssuePriority.find(:first, :conditions => ["id <> ?", i.priority_id])
1266 assert_no_difference 'Journal.count' do
1266 assert_no_difference 'Journal.count' do
1267 assert_difference 'JournalDetail.count', 1 do
1267 assert_difference 'JournalDetail.count', 1 do
1268 i.save
1268 i.save
1269 end
1269 end
1270 end
1270 end
1271 # no more change
1271 # no more change
1272 assert_no_difference 'Journal.count' do
1272 assert_no_difference 'Journal.count' do
1273 assert_no_difference 'JournalDetail.count' do
1273 assert_no_difference 'JournalDetail.count' do
1274 i.save
1274 i.save
1275 end
1275 end
1276 end
1276 end
1277 end
1277 end
1278
1278
1279 def test_all_dependent_issues
1279 def test_all_dependent_issues
1280 IssueRelation.delete_all
1280 IssueRelation.delete_all
1281 assert IssueRelation.create!(:issue_from => Issue.find(1),
1281 assert IssueRelation.create!(:issue_from => Issue.find(1),
1282 :issue_to => Issue.find(2),
1282 :issue_to => Issue.find(2),
1283 :relation_type => IssueRelation::TYPE_PRECEDES)
1283 :relation_type => IssueRelation::TYPE_PRECEDES)
1284 assert IssueRelation.create!(:issue_from => Issue.find(2),
1284 assert IssueRelation.create!(:issue_from => Issue.find(2),
1285 :issue_to => Issue.find(3),
1285 :issue_to => Issue.find(3),
1286 :relation_type => IssueRelation::TYPE_PRECEDES)
1286 :relation_type => IssueRelation::TYPE_PRECEDES)
1287 assert IssueRelation.create!(:issue_from => Issue.find(3),
1287 assert IssueRelation.create!(:issue_from => Issue.find(3),
1288 :issue_to => Issue.find(8),
1288 :issue_to => Issue.find(8),
1289 :relation_type => IssueRelation::TYPE_PRECEDES)
1289 :relation_type => IssueRelation::TYPE_PRECEDES)
1290
1290
1291 assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort
1291 assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort
1292 end
1292 end
1293
1293
1294 def test_all_dependent_issues_with_persistent_circular_dependency
1294 def test_all_dependent_issues_with_persistent_circular_dependency
1295 IssueRelation.delete_all
1295 IssueRelation.delete_all
1296 assert IssueRelation.create!(:issue_from => Issue.find(1),
1296 assert IssueRelation.create!(:issue_from => Issue.find(1),
1297 :issue_to => Issue.find(2),
1297 :issue_to => Issue.find(2),
1298 :relation_type => IssueRelation::TYPE_PRECEDES)
1298 :relation_type => IssueRelation::TYPE_PRECEDES)
1299 assert IssueRelation.create!(:issue_from => Issue.find(2),
1299 assert IssueRelation.create!(:issue_from => Issue.find(2),
1300 :issue_to => Issue.find(3),
1300 :issue_to => Issue.find(3),
1301 :relation_type => IssueRelation::TYPE_PRECEDES)
1301 :relation_type => IssueRelation::TYPE_PRECEDES)
1302
1302
1303 r = IssueRelation.create!(:issue_from => Issue.find(3),
1303 r = IssueRelation.create!(:issue_from => Issue.find(3),
1304 :issue_to => Issue.find(7),
1304 :issue_to => Issue.find(7),
1305 :relation_type => IssueRelation::TYPE_PRECEDES)
1305 :relation_type => IssueRelation::TYPE_PRECEDES)
1306 IssueRelation.update_all("issue_to_id = 1", ["id = ?", r.id])
1306 IssueRelation.update_all("issue_to_id = 1", ["id = ?", r.id])
1307
1307
1308 assert_equal [2, 3], Issue.find(1).all_dependent_issues.collect(&:id).sort
1308 assert_equal [2, 3], Issue.find(1).all_dependent_issues.collect(&:id).sort
1309 end
1309 end
1310
1310
1311 def test_all_dependent_issues_with_persistent_multiple_circular_dependencies
1311 def test_all_dependent_issues_with_persistent_multiple_circular_dependencies
1312 IssueRelation.delete_all
1312 IssueRelation.delete_all
1313 assert IssueRelation.create!(:issue_from => Issue.find(1),
1313 assert IssueRelation.create!(:issue_from => Issue.find(1),
1314 :issue_to => Issue.find(2),
1314 :issue_to => Issue.find(2),
1315 :relation_type => IssueRelation::TYPE_RELATES)
1315 :relation_type => IssueRelation::TYPE_RELATES)
1316 assert IssueRelation.create!(:issue_from => Issue.find(2),
1316 assert IssueRelation.create!(:issue_from => Issue.find(2),
1317 :issue_to => Issue.find(3),
1317 :issue_to => Issue.find(3),
1318 :relation_type => IssueRelation::TYPE_RELATES)
1318 :relation_type => IssueRelation::TYPE_RELATES)
1319 assert IssueRelation.create!(:issue_from => Issue.find(3),
1319 assert IssueRelation.create!(:issue_from => Issue.find(3),
1320 :issue_to => Issue.find(8),
1320 :issue_to => Issue.find(8),
1321 :relation_type => IssueRelation::TYPE_RELATES)
1321 :relation_type => IssueRelation::TYPE_RELATES)
1322
1322
1323 r = IssueRelation.create!(:issue_from => Issue.find(8),
1323 r = IssueRelation.create!(:issue_from => Issue.find(8),
1324 :issue_to => Issue.find(7),
1324 :issue_to => Issue.find(7),
1325 :relation_type => IssueRelation::TYPE_RELATES)
1325 :relation_type => IssueRelation::TYPE_RELATES)
1326 IssueRelation.update_all("issue_to_id = 2", ["id = ?", r.id])
1326 IssueRelation.update_all("issue_to_id = 2", ["id = ?", r.id])
1327
1327
1328 r = IssueRelation.create!(:issue_from => Issue.find(3),
1328 r = IssueRelation.create!(:issue_from => Issue.find(3),
1329 :issue_to => Issue.find(7),
1329 :issue_to => Issue.find(7),
1330 :relation_type => IssueRelation::TYPE_RELATES)
1330 :relation_type => IssueRelation::TYPE_RELATES)
1331 IssueRelation.update_all("issue_to_id = 1", ["id = ?", r.id])
1331 IssueRelation.update_all("issue_to_id = 1", ["id = ?", r.id])
1332
1332
1333 assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort
1333 assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort
1334 end
1334 end
1335
1335
1336 context "#done_ratio" do
1336 context "#done_ratio" do
1337 setup do
1337 setup do
1338 @issue = Issue.find(1)
1338 @issue = Issue.find(1)
1339 @issue_status = IssueStatus.find(1)
1339 @issue_status = IssueStatus.find(1)
1340 @issue_status.update_attribute(:default_done_ratio, 50)
1340 @issue_status.update_attribute(:default_done_ratio, 50)
1341 @issue2 = Issue.find(2)
1341 @issue2 = Issue.find(2)
1342 @issue_status2 = IssueStatus.find(2)
1342 @issue_status2 = IssueStatus.find(2)
1343 @issue_status2.update_attribute(:default_done_ratio, 0)
1343 @issue_status2.update_attribute(:default_done_ratio, 0)
1344 end
1344 end
1345
1345
1346 teardown do
1346 teardown do
1347 Setting.issue_done_ratio = 'issue_field'
1347 Setting.issue_done_ratio = 'issue_field'
1348 end
1348 end
1349
1349
1350 context "with Setting.issue_done_ratio using the issue_field" do
1350 context "with Setting.issue_done_ratio using the issue_field" do
1351 setup do
1351 setup do
1352 Setting.issue_done_ratio = 'issue_field'
1352 Setting.issue_done_ratio = 'issue_field'
1353 end
1353 end
1354
1354
1355 should "read the issue's field" do
1355 should "read the issue's field" do
1356 assert_equal 0, @issue.done_ratio
1356 assert_equal 0, @issue.done_ratio
1357 assert_equal 30, @issue2.done_ratio
1357 assert_equal 30, @issue2.done_ratio
1358 end
1358 end
1359 end
1359 end
1360
1360
1361 context "with Setting.issue_done_ratio using the issue_status" do
1361 context "with Setting.issue_done_ratio using the issue_status" do
1362 setup do
1362 setup do
1363 Setting.issue_done_ratio = 'issue_status'
1363 Setting.issue_done_ratio = 'issue_status'
1364 end
1364 end
1365
1365
1366 should "read the Issue Status's default done ratio" do
1366 should "read the Issue Status's default done ratio" do
1367 assert_equal 50, @issue.done_ratio
1367 assert_equal 50, @issue.done_ratio
1368 assert_equal 0, @issue2.done_ratio
1368 assert_equal 0, @issue2.done_ratio
1369 end
1369 end
1370 end
1370 end
1371 end
1371 end
1372
1372
1373 context "#update_done_ratio_from_issue_status" do
1373 context "#update_done_ratio_from_issue_status" do
1374 setup do
1374 setup do
1375 @issue = Issue.find(1)
1375 @issue = Issue.find(1)
1376 @issue_status = IssueStatus.find(1)
1376 @issue_status = IssueStatus.find(1)
1377 @issue_status.update_attribute(:default_done_ratio, 50)
1377 @issue_status.update_attribute(:default_done_ratio, 50)
1378 @issue2 = Issue.find(2)
1378 @issue2 = Issue.find(2)
1379 @issue_status2 = IssueStatus.find(2)
1379 @issue_status2 = IssueStatus.find(2)
1380 @issue_status2.update_attribute(:default_done_ratio, 0)
1380 @issue_status2.update_attribute(:default_done_ratio, 0)
1381 end
1381 end
1382
1382
1383 context "with Setting.issue_done_ratio using the issue_field" do
1383 context "with Setting.issue_done_ratio using the issue_field" do
1384 setup do
1384 setup do
1385 Setting.issue_done_ratio = 'issue_field'
1385 Setting.issue_done_ratio = 'issue_field'
1386 end
1386 end
1387
1387
1388 should "not change the issue" do
1388 should "not change the issue" do
1389 @issue.update_done_ratio_from_issue_status
1389 @issue.update_done_ratio_from_issue_status
1390 @issue2.update_done_ratio_from_issue_status
1390 @issue2.update_done_ratio_from_issue_status
1391
1391
1392 assert_equal 0, @issue.read_attribute(:done_ratio)
1392 assert_equal 0, @issue.read_attribute(:done_ratio)
1393 assert_equal 30, @issue2.read_attribute(:done_ratio)
1393 assert_equal 30, @issue2.read_attribute(:done_ratio)
1394 end
1394 end
1395 end
1395 end
1396
1396
1397 context "with Setting.issue_done_ratio using the issue_status" do
1397 context "with Setting.issue_done_ratio using the issue_status" do
1398 setup do
1398 setup do
1399 Setting.issue_done_ratio = 'issue_status'
1399 Setting.issue_done_ratio = 'issue_status'
1400 end
1400 end
1401
1401
1402 should "change the issue's done ratio" do
1402 should "change the issue's done ratio" do
1403 @issue.update_done_ratio_from_issue_status
1403 @issue.update_done_ratio_from_issue_status
1404 @issue2.update_done_ratio_from_issue_status
1404 @issue2.update_done_ratio_from_issue_status
1405
1405
1406 assert_equal 50, @issue.read_attribute(:done_ratio)
1406 assert_equal 50, @issue.read_attribute(:done_ratio)
1407 assert_equal 0, @issue2.read_attribute(:done_ratio)
1407 assert_equal 0, @issue2.read_attribute(:done_ratio)
1408 end
1408 end
1409 end
1409 end
1410 end
1410 end
1411
1411
1412 test "#by_tracker" do
1412 test "#by_tracker" do
1413 User.current = User.anonymous
1413 User.current = User.anonymous
1414 groups = Issue.by_tracker(Project.find(1))
1414 groups = Issue.by_tracker(Project.find(1))
1415 assert_equal 3, groups.size
1415 assert_equal 3, groups.size
1416 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1416 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1417 end
1417 end
1418
1418
1419 test "#by_version" do
1419 test "#by_version" do
1420 User.current = User.anonymous
1420 User.current = User.anonymous
1421 groups = Issue.by_version(Project.find(1))
1421 groups = Issue.by_version(Project.find(1))
1422 assert_equal 3, groups.size
1422 assert_equal 3, groups.size
1423 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1423 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1424 end
1424 end
1425
1425
1426 test "#by_priority" do
1426 test "#by_priority" do
1427 User.current = User.anonymous
1427 User.current = User.anonymous
1428 groups = Issue.by_priority(Project.find(1))
1428 groups = Issue.by_priority(Project.find(1))
1429 assert_equal 4, groups.size
1429 assert_equal 4, groups.size
1430 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1430 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1431 end
1431 end
1432
1432
1433 test "#by_category" do
1433 test "#by_category" do
1434 User.current = User.anonymous
1434 User.current = User.anonymous
1435 groups = Issue.by_category(Project.find(1))
1435 groups = Issue.by_category(Project.find(1))
1436 assert_equal 2, groups.size
1436 assert_equal 2, groups.size
1437 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1437 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1438 end
1438 end
1439
1439
1440 test "#by_assigned_to" do
1440 test "#by_assigned_to" do
1441 User.current = User.anonymous
1441 User.current = User.anonymous
1442 groups = Issue.by_assigned_to(Project.find(1))
1442 groups = Issue.by_assigned_to(Project.find(1))
1443 assert_equal 2, groups.size
1443 assert_equal 2, groups.size
1444 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1444 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1445 end
1445 end
1446
1446
1447 test "#by_author" do
1447 test "#by_author" do
1448 User.current = User.anonymous
1448 User.current = User.anonymous
1449 groups = Issue.by_author(Project.find(1))
1449 groups = Issue.by_author(Project.find(1))
1450 assert_equal 4, groups.size
1450 assert_equal 4, groups.size
1451 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1451 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1452 end
1452 end
1453
1453
1454 test "#by_subproject" do
1454 test "#by_subproject" do
1455 User.current = User.anonymous
1455 User.current = User.anonymous
1456 groups = Issue.by_subproject(Project.find(1))
1456 groups = Issue.by_subproject(Project.find(1))
1457 # Private descendant not visible
1457 # Private descendant not visible
1458 assert_equal 1, groups.size
1458 assert_equal 1, groups.size
1459 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1459 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1460 end
1460 end
1461
1461
1462 def test_recently_updated_with_limit_scopes
1462 def test_recently_updated_with_limit_scopes
1463 #should return the last updated issue
1463 #should return the last updated issue
1464 assert_equal 1, Issue.recently_updated.with_limit(1).length
1464 assert_equal 1, Issue.recently_updated.with_limit(1).length
1465 assert_equal Issue.find(:first, :order => "updated_on DESC"), Issue.recently_updated.with_limit(1).first
1465 assert_equal Issue.find(:first, :order => "updated_on DESC"), Issue.recently_updated.with_limit(1).first
1466 end
1466 end
1467
1467
1468 def test_on_active_projects_scope
1468 def test_on_active_projects_scope
1469 assert Project.find(2).archive
1469 assert Project.find(2).archive
1470
1470
1471 before = Issue.on_active_project.length
1471 before = Issue.on_active_project.length
1472 # test inclusion to results
1472 # test inclusion to results
1473 issue = Issue.generate_for_project!(Project.find(1), :tracker => Project.find(2).trackers.first)
1473 issue = Issue.generate_for_project!(Project.find(1), :tracker => Project.find(2).trackers.first)
1474 assert_equal before + 1, Issue.on_active_project.length
1474 assert_equal before + 1, Issue.on_active_project.length
1475
1475
1476 # Move to an archived project
1476 # Move to an archived project
1477 issue.project = Project.find(2)
1477 issue.project = Project.find(2)
1478 assert issue.save
1478 assert issue.save
1479 assert_equal before, Issue.on_active_project.length
1479 assert_equal before, Issue.on_active_project.length
1480 end
1480 end
1481
1481
1482 context "Issue#recipients" do
1482 context "Issue#recipients" do
1483 setup do
1483 setup do
1484 @project = Project.find(1)
1484 @project = Project.find(1)
1485 @author = User.generate!
1485 @author = User.generate!
1486 @assignee = User.generate!
1486 @assignee = User.generate!
1487 @issue = Issue.generate_for_project!(@project, :assigned_to => @assignee, :author => @author)
1487 @issue = Issue.generate_for_project!(@project, :assigned_to => @assignee, :author => @author)
1488 end
1488 end
1489
1489
1490 should "include project recipients" do
1490 should "include project recipients" do
1491 assert @project.recipients.present?
1491 assert @project.recipients.present?
1492 @project.recipients.each do |project_recipient|
1492 @project.recipients.each do |project_recipient|
1493 assert @issue.recipients.include?(project_recipient)
1493 assert @issue.recipients.include?(project_recipient)
1494 end
1494 end
1495 end
1495 end
1496
1496
1497 should "include the author if the author is active" do
1497 should "include the author if the author is active" do
1498 assert @issue.author, "No author set for Issue"
1498 assert @issue.author, "No author set for Issue"
1499 assert @issue.recipients.include?(@issue.author.mail)
1499 assert @issue.recipients.include?(@issue.author.mail)
1500 end
1500 end
1501
1501
1502 should "include the assigned to user if the assigned to user is active" do
1502 should "include the assigned to user if the assigned to user is active" do
1503 assert @issue.assigned_to, "No assigned_to set for Issue"
1503 assert @issue.assigned_to, "No assigned_to set for Issue"
1504 assert @issue.recipients.include?(@issue.assigned_to.mail)
1504 assert @issue.recipients.include?(@issue.assigned_to.mail)
1505 end
1505 end
1506
1506
1507 should "not include users who opt out of all email" do
1507 should "not include users who opt out of all email" do
1508 @author.update_attribute(:mail_notification, :none)
1508 @author.update_attribute(:mail_notification, :none)
1509
1509
1510 assert !@issue.recipients.include?(@issue.author.mail)
1510 assert !@issue.recipients.include?(@issue.author.mail)
1511 end
1511 end
1512
1512
1513 should "not include the issue author if they are only notified of assigned issues" do
1513 should "not include the issue author if they are only notified of assigned issues" do
1514 @author.update_attribute(:mail_notification, :only_assigned)
1514 @author.update_attribute(:mail_notification, :only_assigned)
1515
1515
1516 assert !@issue.recipients.include?(@issue.author.mail)
1516 assert !@issue.recipients.include?(@issue.author.mail)
1517 end
1517 end
1518
1518
1519 should "not include the assigned user if they are only notified of owned issues" do
1519 should "not include the assigned user if they are only notified of owned issues" do
1520 @assignee.update_attribute(:mail_notification, :only_owner)
1520 @assignee.update_attribute(:mail_notification, :only_owner)
1521
1521
1522 assert !@issue.recipients.include?(@issue.assigned_to.mail)
1522 assert !@issue.recipients.include?(@issue.assigned_to.mail)
1523 end
1523 end
1524 end
1524 end
1525
1525
1526 def test_last_journal_id_with_journals_should_return_the_journal_id
1526 def test_last_journal_id_with_journals_should_return_the_journal_id
1527 assert_equal 2, Issue.find(1).last_journal_id
1527 assert_equal 2, Issue.find(1).last_journal_id
1528 end
1528 end
1529
1529
1530 def test_last_journal_id_without_journals_should_return_nil
1530 def test_last_journal_id_without_journals_should_return_nil
1531 assert_nil Issue.find(3).last_journal_id
1531 assert_nil Issue.find(3).last_journal_id
1532 end
1532 end
1533
1534 def test_journals_after_should_return_journals_with_greater_id
1535 assert_equal [Journal.find(2)], Issue.find(1).journals_after('1')
1536 assert_equal [], Issue.find(1).journals_after('2')
1537 end
1538
1539 def test_journals_after_with_blank_arg_should_return_all_journals
1540 assert_equal [Journal.find(1), Journal.find(2)], Issue.find(1).journals_after('')
1541 end
1533 end
1542 end
General Comments 0
You need to be logged in to leave comments. Login now