##// END OF EJS Templates
When copying issues, let the status be changed to default or left unchanged....
Jean-Philippe Lang -
r9270:09375960d69d
parent child
Show More

The requested changes are too big and content was truncated. Show full diff

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