##// END OF EJS Templates
Moved some permission checks for issue update from controller to model....
Jean-Philippe Lang -
r4279:0eb7d8f6149c
parent child
Show More
@@ -1,332 +1,321
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class IssuesController < ApplicationController
18 class IssuesController < ApplicationController
19 menu_item :new_issue, :only => [:new, :create]
19 menu_item :new_issue, :only => [:new, :create]
20 default_search_scope :issues
20 default_search_scope :issues
21
21
22 before_filter :find_issue, :only => [:show, :edit, :update]
22 before_filter :find_issue, :only => [:show, :edit, :update]
23 before_filter :find_issues, :only => [:bulk_edit, :bulk_update, :move, :perform_move, :destroy]
23 before_filter :find_issues, :only => [:bulk_edit, :bulk_update, :move, :perform_move, :destroy]
24 before_filter :check_project_uniqueness, :only => [:move, :perform_move]
24 before_filter :check_project_uniqueness, :only => [:move, :perform_move]
25 before_filter :find_project, :only => [:new, :create]
25 before_filter :find_project, :only => [:new, :create]
26 before_filter :authorize, :except => [:index]
26 before_filter :authorize, :except => [:index]
27 before_filter :find_optional_project, :only => [:index]
27 before_filter :find_optional_project, :only => [:index]
28 before_filter :check_for_default_issue_status, :only => [:new, :create]
28 before_filter :check_for_default_issue_status, :only => [:new, :create]
29 before_filter :build_new_issue_from_params, :only => [:new, :create]
29 before_filter :build_new_issue_from_params, :only => [:new, :create]
30 accept_key_auth :index, :show, :create, :update, :destroy
30 accept_key_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 :sort
47 helper :sort
48 include SortHelper
48 include SortHelper
49 include IssuesHelper
49 include IssuesHelper
50 helper :timelog
50 helper :timelog
51 helper :gantt
51 helper :gantt
52 include Redmine::Export::PDF
52 include Redmine::Export::PDF
53
53
54 verify :method => [:post, :delete],
54 verify :method => [:post, :delete],
55 :only => :destroy,
55 :only => :destroy,
56 :render => { :nothing => true, :status => :method_not_allowed }
56 :render => { :nothing => true, :status => :method_not_allowed }
57
57
58 verify :method => :post, :only => :create, :render => {:nothing => true, :status => :method_not_allowed }
58 verify :method => :post, :only => :create, :render => {:nothing => true, :status => :method_not_allowed }
59 verify :method => :post, :only => :bulk_update, :render => {:nothing => true, :status => :method_not_allowed }
59 verify :method => :post, :only => :bulk_update, :render => {:nothing => true, :status => :method_not_allowed }
60 verify :method => :put, :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
60 verify :method => :put, :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
61
61
62 def index
62 def index
63 retrieve_query
63 retrieve_query
64 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
64 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
65 sort_update(@query.sortable_columns)
65 sort_update(@query.sortable_columns)
66
66
67 if @query.valid?
67 if @query.valid?
68 limit = case params[:format]
68 limit = case params[:format]
69 when 'csv', 'pdf'
69 when 'csv', 'pdf'
70 Setting.issues_export_limit.to_i
70 Setting.issues_export_limit.to_i
71 when 'atom'
71 when 'atom'
72 Setting.feeds_limit.to_i
72 Setting.feeds_limit.to_i
73 else
73 else
74 per_page_option
74 per_page_option
75 end
75 end
76
76
77 @issue_count = @query.issue_count
77 @issue_count = @query.issue_count
78 @issue_pages = Paginator.new self, @issue_count, limit, params['page']
78 @issue_pages = Paginator.new self, @issue_count, limit, params['page']
79 @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
79 @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
80 :order => sort_clause,
80 :order => sort_clause,
81 :offset => @issue_pages.current.offset,
81 :offset => @issue_pages.current.offset,
82 :limit => limit)
82 :limit => limit)
83 @issue_count_by_group = @query.issue_count_by_group
83 @issue_count_by_group = @query.issue_count_by_group
84
84
85 respond_to do |format|
85 respond_to do |format|
86 format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
86 format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
87 format.xml { render :layout => false }
87 format.xml { render :layout => false }
88 format.json { render :text => @issues.to_json, :layout => false }
88 format.json { render :text => @issues.to_json, :layout => false }
89 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
89 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
90 format.csv { send_data(issues_to_csv(@issues, @project), :type => 'text/csv; header=present', :filename => 'export.csv') }
90 format.csv { send_data(issues_to_csv(@issues, @project), :type => 'text/csv; header=present', :filename => 'export.csv') }
91 format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'export.pdf') }
91 format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'export.pdf') }
92 end
92 end
93 else
93 else
94 # Send html if the query is not valid
94 # Send html if the query is not valid
95 render(:template => 'issues/index.rhtml', :layout => !request.xhr?)
95 render(:template => 'issues/index.rhtml', :layout => !request.xhr?)
96 end
96 end
97 rescue ActiveRecord::RecordNotFound
97 rescue ActiveRecord::RecordNotFound
98 render_404
98 render_404
99 end
99 end
100
100
101 def show
101 def show
102 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
102 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
103 @journals.each_with_index {|j,i| j.indice = i+1}
103 @journals.each_with_index {|j,i| j.indice = i+1}
104 @journals.reverse! if User.current.wants_comments_in_reverse_order?
104 @journals.reverse! if User.current.wants_comments_in_reverse_order?
105 @changesets = @issue.changesets.visible.all
105 @changesets = @issue.changesets.visible.all
106 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
106 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
107 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
107 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
108 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
108 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
109 @priorities = IssuePriority.all
109 @priorities = IssuePriority.all
110 @time_entry = TimeEntry.new
110 @time_entry = TimeEntry.new
111 respond_to do |format|
111 respond_to do |format|
112 format.html { render :template => 'issues/show.rhtml' }
112 format.html { render :template => 'issues/show.rhtml' }
113 format.xml { render :layout => false }
113 format.xml { render :layout => false }
114 format.json { render :text => @issue.to_json, :layout => false }
114 format.json { render :text => @issue.to_json, :layout => false }
115 format.atom { render :template => 'journals/index', :layout => false, :content_type => 'application/atom+xml' }
115 format.atom { render :template => 'journals/index', :layout => false, :content_type => 'application/atom+xml' }
116 format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
116 format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
117 end
117 end
118 end
118 end
119
119
120 # Add a new issue
120 # Add a new issue
121 # The new issue will be created from an existing one if copy_from parameter is given
121 # The new issue will be created from an existing one if copy_from parameter is given
122 def new
122 def new
123 respond_to do |format|
123 respond_to do |format|
124 format.html { render :action => 'new', :layout => !request.xhr? }
124 format.html { render :action => 'new', :layout => !request.xhr? }
125 format.js { render :partial => 'attributes' }
125 format.js { render :partial => 'attributes' }
126 end
126 end
127 end
127 end
128
128
129 def create
129 def create
130 call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
130 call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
131 if @issue.save
131 if @issue.save
132 attachments = Attachment.attach_files(@issue, params[:attachments])
132 attachments = Attachment.attach_files(@issue, params[:attachments])
133 render_attachment_warning_if_needed(@issue)
133 render_attachment_warning_if_needed(@issue)
134 flash[:notice] = l(:notice_successful_create)
134 flash[:notice] = l(:notice_successful_create)
135 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
135 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
136 respond_to do |format|
136 respond_to do |format|
137 format.html {
137 format.html {
138 redirect_to(params[:continue] ? { :action => 'new', :project_id => @project, :issue => {:tracker_id => @issue.tracker, :parent_issue_id => @issue.parent_issue_id}.reject {|k,v| v.nil?} } :
138 redirect_to(params[:continue] ? { :action => 'new', :project_id => @project, :issue => {:tracker_id => @issue.tracker, :parent_issue_id => @issue.parent_issue_id}.reject {|k,v| v.nil?} } :
139 { :action => 'show', :id => @issue })
139 { :action => 'show', :id => @issue })
140 }
140 }
141 format.xml { render :action => 'show', :status => :created, :location => url_for(:controller => 'issues', :action => 'show', :id => @issue) }
141 format.xml { render :action => 'show', :status => :created, :location => url_for(:controller => 'issues', :action => 'show', :id => @issue) }
142 format.json { render :text => @issue.to_json, :status => :created, :location => url_for(:controller => 'issues', :action => 'show'), :layout => false }
142 format.json { render :text => @issue.to_json, :status => :created, :location => url_for(:controller => 'issues', :action => 'show'), :layout => false }
143 end
143 end
144 return
144 return
145 else
145 else
146 respond_to do |format|
146 respond_to do |format|
147 format.html { render :action => 'new' }
147 format.html { render :action => 'new' }
148 format.xml { render(:xml => @issue.errors, :status => :unprocessable_entity); return }
148 format.xml { render(:xml => @issue.errors, :status => :unprocessable_entity); return }
149 format.json { render :text => object_errors_to_json(@issue), :status => :unprocessable_entity, :layout => false }
149 format.json { render :text => object_errors_to_json(@issue), :status => :unprocessable_entity, :layout => false }
150 end
150 end
151 end
151 end
152 end
152 end
153
153
154 # Attributes that can be updated on workflow transition (without :edit permission)
155 # TODO: make it configurable (at least per role)
156 UPDATABLE_ATTRS_ON_TRANSITION = %w(status_id assigned_to_id fixed_version_id done_ratio) unless const_defined?(:UPDATABLE_ATTRS_ON_TRANSITION)
157
158 def edit
154 def edit
159 update_issue_from_params
155 update_issue_from_params
160
156
161 @journal = @issue.current_journal
157 @journal = @issue.current_journal
162
158
163 respond_to do |format|
159 respond_to do |format|
164 format.html { }
160 format.html { }
165 format.xml { }
161 format.xml { }
166 end
162 end
167 end
163 end
168
164
169 def update
165 def update
170 update_issue_from_params
166 update_issue_from_params
171
167
172 if @issue.save_issue_with_child_records(params, @time_entry)
168 if @issue.save_issue_with_child_records(params, @time_entry)
173 render_attachment_warning_if_needed(@issue)
169 render_attachment_warning_if_needed(@issue)
174 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
170 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
175
171
176 respond_to do |format|
172 respond_to do |format|
177 format.html { redirect_back_or_default({:action => 'show', :id => @issue}) }
173 format.html { redirect_back_or_default({:action => 'show', :id => @issue}) }
178 format.xml { head :ok }
174 format.xml { head :ok }
179 format.json { head :ok }
175 format.json { head :ok }
180 end
176 end
181 else
177 else
182 render_attachment_warning_if_needed(@issue)
178 render_attachment_warning_if_needed(@issue)
183 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
179 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
184 @journal = @issue.current_journal
180 @journal = @issue.current_journal
185
181
186 respond_to do |format|
182 respond_to do |format|
187 format.html { render :action => 'edit' }
183 format.html { render :action => 'edit' }
188 format.xml { render :xml => @issue.errors, :status => :unprocessable_entity }
184 format.xml { render :xml => @issue.errors, :status => :unprocessable_entity }
189 format.json { render :text => object_errors_to_json(@issue), :status => :unprocessable_entity, :layout => false }
185 format.json { render :text => object_errors_to_json(@issue), :status => :unprocessable_entity, :layout => false }
190 end
186 end
191 end
187 end
192 end
188 end
193
189
194 # Bulk edit a set of issues
190 # Bulk edit a set of issues
195 def bulk_edit
191 def bulk_edit
196 @issues.sort!
192 @issues.sort!
197 @available_statuses = @projects.map{|p|Workflow.available_statuses(p)}.inject{|memo,w|memo & w}
193 @available_statuses = @projects.map{|p|Workflow.available_statuses(p)}.inject{|memo,w|memo & w}
198 @custom_fields = @projects.map{|p|p.all_issue_custom_fields}.inject{|memo,c|memo & c}
194 @custom_fields = @projects.map{|p|p.all_issue_custom_fields}.inject{|memo,c|memo & c}
199 @assignables = @projects.map(&:assignable_users).inject{|memo,a| memo & a}
195 @assignables = @projects.map(&:assignable_users).inject{|memo,a| memo & a}
200 @trackers = @projects.map(&:trackers).inject{|memo,t| memo & t}
196 @trackers = @projects.map(&:trackers).inject{|memo,t| memo & t}
201 end
197 end
202
198
203 def bulk_update
199 def bulk_update
204 @issues.sort!
200 @issues.sort!
205 attributes = parse_params_for_bulk_issue_attributes(params)
201 attributes = parse_params_for_bulk_issue_attributes(params)
206
202
207 unsaved_issue_ids = []
203 unsaved_issue_ids = []
208 @issues.each do |issue|
204 @issues.each do |issue|
209 issue.reload
205 issue.reload
210 journal = issue.init_journal(User.current, params[:notes])
206 journal = issue.init_journal(User.current, params[:notes])
211 issue.safe_attributes = attributes
207 issue.safe_attributes = attributes
212 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
208 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
213 unless issue.save
209 unless issue.save
214 # Keep unsaved issue ids to display them in flash error
210 # Keep unsaved issue ids to display them in flash error
215 unsaved_issue_ids << issue.id
211 unsaved_issue_ids << issue.id
216 end
212 end
217 end
213 end
218 set_flash_from_bulk_issue_save(@issues, unsaved_issue_ids)
214 set_flash_from_bulk_issue_save(@issues, unsaved_issue_ids)
219 redirect_back_or_default({:controller => 'issues', :action => 'index', :project_id => @project})
215 redirect_back_or_default({:controller => 'issues', :action => 'index', :project_id => @project})
220 end
216 end
221
217
222 def destroy
218 def destroy
223 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
219 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
224 if @hours > 0
220 if @hours > 0
225 case params[:todo]
221 case params[:todo]
226 when 'destroy'
222 when 'destroy'
227 # nothing to do
223 # nothing to do
228 when 'nullify'
224 when 'nullify'
229 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
225 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
230 when 'reassign'
226 when 'reassign'
231 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
227 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
232 if reassign_to.nil?
228 if reassign_to.nil?
233 flash.now[:error] = l(:error_issue_not_found_in_project)
229 flash.now[:error] = l(:error_issue_not_found_in_project)
234 return
230 return
235 else
231 else
236 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
232 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
237 end
233 end
238 else
234 else
239 unless params[:format] == 'xml' || params[:format] == 'json'
235 unless params[:format] == 'xml' || params[:format] == 'json'
240 # display the destroy form if it's a user request
236 # display the destroy form if it's a user request
241 return
237 return
242 end
238 end
243 end
239 end
244 end
240 end
245 @issues.each(&:destroy)
241 @issues.each(&:destroy)
246 respond_to do |format|
242 respond_to do |format|
247 format.html { redirect_back_or_default(:action => 'index', :project_id => @project) }
243 format.html { redirect_back_or_default(:action => 'index', :project_id => @project) }
248 format.xml { head :ok }
244 format.xml { head :ok }
249 format.json { head :ok }
245 format.json { head :ok }
250 end
246 end
251 end
247 end
252
248
253 private
249 private
254 def find_issue
250 def find_issue
255 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
251 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
256 @project = @issue.project
252 @project = @issue.project
257 rescue ActiveRecord::RecordNotFound
253 rescue ActiveRecord::RecordNotFound
258 render_404
254 render_404
259 end
255 end
260
256
261 def find_project
257 def find_project
262 project_id = (params[:issue] && params[:issue][:project_id]) || params[:project_id]
258 project_id = (params[:issue] && params[:issue][:project_id]) || params[:project_id]
263 @project = Project.find(project_id)
259 @project = Project.find(project_id)
264 rescue ActiveRecord::RecordNotFound
260 rescue ActiveRecord::RecordNotFound
265 render_404
261 render_404
266 end
262 end
267
263
268 # Used by #edit and #update to set some common instance variables
264 # Used by #edit and #update to set some common instance variables
269 # from the params
265 # from the params
270 # TODO: Refactor, not everything in here is needed by #edit
266 # TODO: Refactor, not everything in here is needed by #edit
271 def update_issue_from_params
267 def update_issue_from_params
272 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
268 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
273 @priorities = IssuePriority.all
269 @priorities = IssuePriority.all
274 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
270 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
275 @time_entry = TimeEntry.new
271 @time_entry = TimeEntry.new
276
272
277 @notes = params[:notes] || (params[:issue].present? ? params[:issue][:notes] : nil)
273 @notes = params[:notes] || (params[:issue].present? ? params[:issue][:notes] : nil)
278 @issue.init_journal(User.current, @notes)
274 @issue.init_journal(User.current, @notes)
279 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
275 @issue.safe_attributes = params[:issue]
280 if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue]
281 attrs = params[:issue].dup
282 attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed
283 attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s}
284 @issue.safe_attributes = attrs
285 end
286
287 end
276 end
288
277
289 # TODO: Refactor, lots of extra code in here
278 # TODO: Refactor, lots of extra code in here
290 # TODO: Changing tracker on an existing issue should not trigger this
279 # TODO: Changing tracker on an existing issue should not trigger this
291 def build_new_issue_from_params
280 def build_new_issue_from_params
292 if params[:id].blank?
281 if params[:id].blank?
293 @issue = Issue.new
282 @issue = Issue.new
294 @issue.copy_from(params[:copy_from]) if params[:copy_from]
283 @issue.copy_from(params[:copy_from]) if params[:copy_from]
295 @issue.project = @project
284 @issue.project = @project
296 else
285 else
297 @issue = @project.issues.visible.find(params[:id])
286 @issue = @project.issues.visible.find(params[:id])
298 end
287 end
299
288
300 @issue.project = @project
289 @issue.project = @project
301 # Tracker must be set before custom field values
290 # Tracker must be set before custom field values
302 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
291 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
303 if @issue.tracker.nil?
292 if @issue.tracker.nil?
304 render_error l(:error_no_tracker_in_project)
293 render_error l(:error_no_tracker_in_project)
305 return false
294 return false
306 end
295 end
307 @issue.start_date ||= Date.today
296 @issue.start_date ||= Date.today
308 if params[:issue].is_a?(Hash)
297 if params[:issue].is_a?(Hash)
309 @issue.safe_attributes = params[:issue]
298 @issue.safe_attributes = params[:issue]
310 if User.current.allowed_to?(:add_issue_watchers, @project) && @issue.new_record?
299 if User.current.allowed_to?(:add_issue_watchers, @project) && @issue.new_record?
311 @issue.watcher_user_ids = params[:issue]['watcher_user_ids']
300 @issue.watcher_user_ids = params[:issue]['watcher_user_ids']
312 end
301 end
313 end
302 end
314 @issue.author = User.current
303 @issue.author = User.current
315 @priorities = IssuePriority.all
304 @priorities = IssuePriority.all
316 @allowed_statuses = @issue.new_statuses_allowed_to(User.current, true)
305 @allowed_statuses = @issue.new_statuses_allowed_to(User.current, true)
317 end
306 end
318
307
319 def check_for_default_issue_status
308 def check_for_default_issue_status
320 if IssueStatus.default.nil?
309 if IssueStatus.default.nil?
321 render_error l(:error_no_default_issue_status)
310 render_error l(:error_no_default_issue_status)
322 return false
311 return false
323 end
312 end
324 end
313 end
325
314
326 def parse_params_for_bulk_issue_attributes(params)
315 def parse_params_for_bulk_issue_attributes(params)
327 attributes = (params[:issue] || {}).reject {|k,v| v.blank?}
316 attributes = (params[:issue] || {}).reject {|k,v| v.blank?}
328 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
317 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
329 attributes[:custom_field_values].reject! {|k,v| v.blank?} if attributes[:custom_field_values]
318 attributes[:custom_field_values].reject! {|k,v| v.blank?} if attributes[:custom_field_values]
330 attributes
319 attributes
331 end
320 end
332 end
321 end
@@ -1,866 +1,884
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 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 belongs_to :project
19 belongs_to :project
20 belongs_to :tracker
20 belongs_to :tracker
21 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
21 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
22 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
22 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
23 belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id'
23 belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id'
24 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
24 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
25 belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
25 belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
26 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
26 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
27
27
28 has_many :journals, :as => :journalized, :dependent => :destroy
28 has_many :journals, :as => :journalized, :dependent => :destroy
29 has_many :time_entries, :dependent => :delete_all
29 has_many :time_entries, :dependent => :delete_all
30 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
30 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
31
31
32 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
32 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
33 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
33 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
34
34
35 acts_as_nested_set :scope => 'root_id'
35 acts_as_nested_set :scope => 'root_id'
36 acts_as_attachable :after_remove => :attachment_removed
36 acts_as_attachable :after_remove => :attachment_removed
37 acts_as_customizable
37 acts_as_customizable
38 acts_as_watchable
38 acts_as_watchable
39 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
39 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
40 :include => [:project, :journals],
40 :include => [:project, :journals],
41 # sort by id so that limited eager loading doesn't break with postgresql
41 # sort by id so that limited eager loading doesn't break with postgresql
42 :order_column => "#{table_name}.id"
42 :order_column => "#{table_name}.id"
43 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
43 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
44 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
44 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
45 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
45 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
46
46
47 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
47 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
48 :author_key => :author_id
48 :author_key => :author_id
49
49
50 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
50 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
51
51
52 attr_reader :current_journal
52 attr_reader :current_journal
53
53
54 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
54 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
55
55
56 validates_length_of :subject, :maximum => 255
56 validates_length_of :subject, :maximum => 255
57 validates_inclusion_of :done_ratio, :in => 0..100
57 validates_inclusion_of :done_ratio, :in => 0..100
58 validates_numericality_of :estimated_hours, :allow_nil => true
58 validates_numericality_of :estimated_hours, :allow_nil => true
59
59
60 named_scope :visible, lambda {|*args| { :include => :project,
60 named_scope :visible, lambda {|*args| { :include => :project,
61 :conditions => Project.allowed_to_condition(args.first || User.current, :view_issues) } }
61 :conditions => Project.allowed_to_condition(args.first || User.current, :view_issues) } }
62
62
63 named_scope :open, :conditions => ["#{IssueStatus.table_name}.is_closed = ?", false], :include => :status
63 named_scope :open, :conditions => ["#{IssueStatus.table_name}.is_closed = ?", false], :include => :status
64
64
65 named_scope :recently_updated, :order => "#{Issue.table_name}.updated_on DESC"
65 named_scope :recently_updated, :order => "#{Issue.table_name}.updated_on DESC"
66 named_scope :with_limit, lambda { |limit| { :limit => limit} }
66 named_scope :with_limit, lambda { |limit| { :limit => limit} }
67 named_scope :on_active_project, :include => [:status, :project, :tracker],
67 named_scope :on_active_project, :include => [:status, :project, :tracker],
68 :conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"]
68 :conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"]
69 named_scope :for_gantt, lambda {
69 named_scope :for_gantt, lambda {
70 {
70 {
71 :include => [:tracker, :status, :assigned_to, :priority, :project, :fixed_version],
71 :include => [:tracker, :status, :assigned_to, :priority, :project, :fixed_version],
72 :order => "#{Issue.table_name}.due_date ASC, #{Issue.table_name}.start_date ASC, #{Issue.table_name}.id ASC"
72 :order => "#{Issue.table_name}.due_date ASC, #{Issue.table_name}.start_date ASC, #{Issue.table_name}.id ASC"
73 }
73 }
74 }
74 }
75
75
76 named_scope :without_version, lambda {
76 named_scope :without_version, lambda {
77 {
77 {
78 :conditions => { :fixed_version_id => nil}
78 :conditions => { :fixed_version_id => nil}
79 }
79 }
80 }
80 }
81
81
82 named_scope :with_query, lambda {|query|
82 named_scope :with_query, lambda {|query|
83 {
83 {
84 :conditions => Query.merge_conditions(query.statement)
84 :conditions => Query.merge_conditions(query.statement)
85 }
85 }
86 }
86 }
87
87
88 before_create :default_assign
88 before_create :default_assign
89 before_save :close_duplicates, :update_done_ratio_from_issue_status
89 before_save :close_duplicates, :update_done_ratio_from_issue_status
90 after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal
90 after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal
91 after_destroy :destroy_children
91 after_destroy :destroy_children
92 after_destroy :update_parent_attributes
92 after_destroy :update_parent_attributes
93
93
94 # Returns true if usr or current user is allowed to view the issue
94 # Returns true if usr or current user is allowed to view the issue
95 def visible?(usr=nil)
95 def visible?(usr=nil)
96 (usr || User.current).allowed_to?(:view_issues, self.project)
96 (usr || User.current).allowed_to?(:view_issues, self.project)
97 end
97 end
98
98
99 def after_initialize
99 def after_initialize
100 if new_record?
100 if new_record?
101 # set default values for new records only
101 # set default values for new records only
102 self.status ||= IssueStatus.default
102 self.status ||= IssueStatus.default
103 self.priority ||= IssuePriority.default
103 self.priority ||= IssuePriority.default
104 end
104 end
105 end
105 end
106
106
107 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
107 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
108 def available_custom_fields
108 def available_custom_fields
109 (project && tracker) ? project.all_issue_custom_fields.select {|c| tracker.custom_fields.include? c } : []
109 (project && tracker) ? project.all_issue_custom_fields.select {|c| tracker.custom_fields.include? c } : []
110 end
110 end
111
111
112 def copy_from(arg)
112 def copy_from(arg)
113 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
113 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
114 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
114 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
115 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
115 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
116 self.status = issue.status
116 self.status = issue.status
117 self
117 self
118 end
118 end
119
119
120 # Moves/copies an issue to a new project and tracker
120 # Moves/copies an issue to a new project and tracker
121 # Returns the moved/copied issue on success, false on failure
121 # Returns the moved/copied issue on success, false on failure
122 def move_to_project(*args)
122 def move_to_project(*args)
123 ret = Issue.transaction do
123 ret = Issue.transaction do
124 move_to_project_without_transaction(*args) || raise(ActiveRecord::Rollback)
124 move_to_project_without_transaction(*args) || raise(ActiveRecord::Rollback)
125 end || false
125 end || false
126 end
126 end
127
127
128 def move_to_project_without_transaction(new_project, new_tracker = nil, options = {})
128 def move_to_project_without_transaction(new_project, new_tracker = nil, options = {})
129 options ||= {}
129 options ||= {}
130 issue = options[:copy] ? self.class.new.copy_from(self) : self
130 issue = options[:copy] ? self.class.new.copy_from(self) : self
131
131
132 if new_project && issue.project_id != new_project.id
132 if new_project && issue.project_id != new_project.id
133 # delete issue relations
133 # delete issue relations
134 unless Setting.cross_project_issue_relations?
134 unless Setting.cross_project_issue_relations?
135 issue.relations_from.clear
135 issue.relations_from.clear
136 issue.relations_to.clear
136 issue.relations_to.clear
137 end
137 end
138 # issue is moved to another project
138 # issue is moved to another project
139 # reassign to the category with same name if any
139 # reassign to the category with same name if any
140 new_category = issue.category.nil? ? nil : new_project.issue_categories.find_by_name(issue.category.name)
140 new_category = issue.category.nil? ? nil : new_project.issue_categories.find_by_name(issue.category.name)
141 issue.category = new_category
141 issue.category = new_category
142 # Keep the fixed_version if it's still valid in the new_project
142 # Keep the fixed_version if it's still valid in the new_project
143 unless new_project.shared_versions.include?(issue.fixed_version)
143 unless new_project.shared_versions.include?(issue.fixed_version)
144 issue.fixed_version = nil
144 issue.fixed_version = nil
145 end
145 end
146 issue.project = new_project
146 issue.project = new_project
147 if issue.parent && issue.parent.project_id != issue.project_id
147 if issue.parent && issue.parent.project_id != issue.project_id
148 issue.parent_issue_id = nil
148 issue.parent_issue_id = nil
149 end
149 end
150 end
150 end
151 if new_tracker
151 if new_tracker
152 issue.tracker = new_tracker
152 issue.tracker = new_tracker
153 issue.reset_custom_values!
153 issue.reset_custom_values!
154 end
154 end
155 if options[:copy]
155 if options[:copy]
156 issue.custom_field_values = self.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
156 issue.custom_field_values = self.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
157 issue.status = if options[:attributes] && options[:attributes][:status_id]
157 issue.status = if options[:attributes] && options[:attributes][:status_id]
158 IssueStatus.find_by_id(options[:attributes][:status_id])
158 IssueStatus.find_by_id(options[:attributes][:status_id])
159 else
159 else
160 self.status
160 self.status
161 end
161 end
162 end
162 end
163 # Allow bulk setting of attributes on the issue
163 # Allow bulk setting of attributes on the issue
164 if options[:attributes]
164 if options[:attributes]
165 issue.attributes = options[:attributes]
165 issue.attributes = options[:attributes]
166 end
166 end
167 if issue.save
167 if issue.save
168 unless options[:copy]
168 unless options[:copy]
169 # Manually update project_id on related time entries
169 # Manually update project_id on related time entries
170 TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
170 TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
171
171
172 issue.children.each do |child|
172 issue.children.each do |child|
173 unless child.move_to_project_without_transaction(new_project)
173 unless child.move_to_project_without_transaction(new_project)
174 # Move failed and transaction was rollback'd
174 # Move failed and transaction was rollback'd
175 return false
175 return false
176 end
176 end
177 end
177 end
178 end
178 end
179 else
179 else
180 return false
180 return false
181 end
181 end
182 issue
182 issue
183 end
183 end
184
184
185 def status_id=(sid)
185 def status_id=(sid)
186 self.status = nil
186 self.status = nil
187 write_attribute(:status_id, sid)
187 write_attribute(:status_id, sid)
188 end
188 end
189
189
190 def priority_id=(pid)
190 def priority_id=(pid)
191 self.priority = nil
191 self.priority = nil
192 write_attribute(:priority_id, pid)
192 write_attribute(:priority_id, pid)
193 end
193 end
194
194
195 def tracker_id=(tid)
195 def tracker_id=(tid)
196 self.tracker = nil
196 self.tracker = nil
197 result = write_attribute(:tracker_id, tid)
197 result = write_attribute(:tracker_id, tid)
198 @custom_field_values = nil
198 @custom_field_values = nil
199 result
199 result
200 end
200 end
201
201
202 # Overrides attributes= so that tracker_id gets assigned first
202 # Overrides attributes= so that tracker_id gets assigned first
203 def attributes_with_tracker_first=(new_attributes, *args)
203 def attributes_with_tracker_first=(new_attributes, *args)
204 return if new_attributes.nil?
204 return if new_attributes.nil?
205 new_tracker_id = new_attributes['tracker_id'] || new_attributes[:tracker_id]
205 new_tracker_id = new_attributes['tracker_id'] || new_attributes[:tracker_id]
206 if new_tracker_id
206 if new_tracker_id
207 self.tracker_id = new_tracker_id
207 self.tracker_id = new_tracker_id
208 end
208 end
209 send :attributes_without_tracker_first=, new_attributes, *args
209 send :attributes_without_tracker_first=, new_attributes, *args
210 end
210 end
211 # Do not redefine alias chain on reload (see #4838)
211 # Do not redefine alias chain on reload (see #4838)
212 alias_method_chain(:attributes=, :tracker_first) unless method_defined?(:attributes_without_tracker_first=)
212 alias_method_chain(:attributes=, :tracker_first) unless method_defined?(:attributes_without_tracker_first=)
213
213
214 def estimated_hours=(h)
214 def estimated_hours=(h)
215 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
215 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
216 end
216 end
217
217
218 SAFE_ATTRIBUTES = %w(
218 SAFE_ATTRIBUTES = %w(
219 tracker_id
219 tracker_id
220 status_id
220 status_id
221 parent_issue_id
221 parent_issue_id
222 category_id
222 category_id
223 assigned_to_id
223 assigned_to_id
224 priority_id
224 priority_id
225 fixed_version_id
225 fixed_version_id
226 subject
226 subject
227 description
227 description
228 start_date
228 start_date
229 due_date
229 due_date
230 done_ratio
230 done_ratio
231 estimated_hours
231 estimated_hours
232 custom_field_values
232 custom_field_values
233 lock_version
233 lock_version
234 ) unless const_defined?(:SAFE_ATTRIBUTES)
234 ) unless const_defined?(:SAFE_ATTRIBUTES)
235
235
236 SAFE_ATTRIBUTES_ON_TRANSITION = %w(
237 status_id
238 assigned_to_id
239 fixed_version_id
240 done_ratio
241 ) unless const_defined?(:SAFE_ATTRIBUTES_ON_TRANSITION)
242
236 # Safely sets attributes
243 # Safely sets attributes
237 # Should be called from controllers instead of #attributes=
244 # Should be called from controllers instead of #attributes=
238 # attr_accessible is too rough because we still want things like
245 # attr_accessible is too rough because we still want things like
239 # Issue.new(:project => foo) to work
246 # Issue.new(:project => foo) to work
240 # TODO: move workflow/permission checks from controllers to here
247 # TODO: move workflow/permission checks from controllers to here
241 def safe_attributes=(attrs, user=User.current)
248 def safe_attributes=(attrs, user=User.current)
242 return if attrs.nil?
249 return unless attrs.is_a?(Hash)
243 attrs = attrs.reject {|k,v| !SAFE_ATTRIBUTES.include?(k)}
250
251 new_statuses_allowed = new_statuses_allowed_to(user)
252
253 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
254 if new_record? || user.allowed_to?(:edit_issues, project)
255 attrs = attrs.reject {|k,v| !SAFE_ATTRIBUTES.include?(k)}
256 elsif new_statuses_allowed.any?
257 attrs = attrs.reject {|k,v| !SAFE_ATTRIBUTES_ON_TRANSITION.include?(k)}
258 else
259 return
260 end
261
244 if attrs['status_id']
262 if attrs['status_id']
245 unless new_statuses_allowed_to(user).collect(&:id).include?(attrs['status_id'].to_i)
263 unless new_statuses_allowed.collect(&:id).include?(attrs['status_id'].to_i)
246 attrs.delete('status_id')
264 attrs.delete('status_id')
247 end
265 end
248 end
266 end
249
267
250 unless leaf?
268 unless leaf?
251 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
269 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
252 end
270 end
253
271
254 if attrs.has_key?('parent_issue_id')
272 if attrs.has_key?('parent_issue_id')
255 if !user.allowed_to?(:manage_subtasks, project)
273 if !user.allowed_to?(:manage_subtasks, project)
256 attrs.delete('parent_issue_id')
274 attrs.delete('parent_issue_id')
257 elsif !attrs['parent_issue_id'].blank?
275 elsif !attrs['parent_issue_id'].blank?
258 attrs.delete('parent_issue_id') unless Issue.visible(user).exists?(attrs['parent_issue_id'])
276 attrs.delete('parent_issue_id') unless Issue.visible(user).exists?(attrs['parent_issue_id'])
259 end
277 end
260 end
278 end
261
279
262 self.attributes = attrs
280 self.attributes = attrs
263 end
281 end
264
282
265 def done_ratio
283 def done_ratio
266 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
284 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
267 status.default_done_ratio
285 status.default_done_ratio
268 else
286 else
269 read_attribute(:done_ratio)
287 read_attribute(:done_ratio)
270 end
288 end
271 end
289 end
272
290
273 def self.use_status_for_done_ratio?
291 def self.use_status_for_done_ratio?
274 Setting.issue_done_ratio == 'issue_status'
292 Setting.issue_done_ratio == 'issue_status'
275 end
293 end
276
294
277 def self.use_field_for_done_ratio?
295 def self.use_field_for_done_ratio?
278 Setting.issue_done_ratio == 'issue_field'
296 Setting.issue_done_ratio == 'issue_field'
279 end
297 end
280
298
281 def validate
299 def validate
282 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
300 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
283 errors.add :due_date, :not_a_date
301 errors.add :due_date, :not_a_date
284 end
302 end
285
303
286 if self.due_date and self.start_date and self.due_date < self.start_date
304 if self.due_date and self.start_date and self.due_date < self.start_date
287 errors.add :due_date, :greater_than_start_date
305 errors.add :due_date, :greater_than_start_date
288 end
306 end
289
307
290 if start_date && soonest_start && start_date < soonest_start
308 if start_date && soonest_start && start_date < soonest_start
291 errors.add :start_date, :invalid
309 errors.add :start_date, :invalid
292 end
310 end
293
311
294 if fixed_version
312 if fixed_version
295 if !assignable_versions.include?(fixed_version)
313 if !assignable_versions.include?(fixed_version)
296 errors.add :fixed_version_id, :inclusion
314 errors.add :fixed_version_id, :inclusion
297 elsif reopened? && fixed_version.closed?
315 elsif reopened? && fixed_version.closed?
298 errors.add_to_base I18n.t(:error_can_not_reopen_issue_on_closed_version)
316 errors.add_to_base I18n.t(:error_can_not_reopen_issue_on_closed_version)
299 end
317 end
300 end
318 end
301
319
302 # Checks that the issue can not be added/moved to a disabled tracker
320 # Checks that the issue can not be added/moved to a disabled tracker
303 if project && (tracker_id_changed? || project_id_changed?)
321 if project && (tracker_id_changed? || project_id_changed?)
304 unless project.trackers.include?(tracker)
322 unless project.trackers.include?(tracker)
305 errors.add :tracker_id, :inclusion
323 errors.add :tracker_id, :inclusion
306 end
324 end
307 end
325 end
308
326
309 # Checks parent issue assignment
327 # Checks parent issue assignment
310 if @parent_issue
328 if @parent_issue
311 if @parent_issue.project_id != project_id
329 if @parent_issue.project_id != project_id
312 errors.add :parent_issue_id, :not_same_project
330 errors.add :parent_issue_id, :not_same_project
313 elsif !new_record?
331 elsif !new_record?
314 # moving an existing issue
332 # moving an existing issue
315 if @parent_issue.root_id != root_id
333 if @parent_issue.root_id != root_id
316 # we can always move to another tree
334 # we can always move to another tree
317 elsif move_possible?(@parent_issue)
335 elsif move_possible?(@parent_issue)
318 # move accepted inside tree
336 # move accepted inside tree
319 else
337 else
320 errors.add :parent_issue_id, :not_a_valid_parent
338 errors.add :parent_issue_id, :not_a_valid_parent
321 end
339 end
322 end
340 end
323 end
341 end
324 end
342 end
325
343
326 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
344 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
327 # even if the user turns off the setting later
345 # even if the user turns off the setting later
328 def update_done_ratio_from_issue_status
346 def update_done_ratio_from_issue_status
329 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
347 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
330 self.done_ratio = status.default_done_ratio
348 self.done_ratio = status.default_done_ratio
331 end
349 end
332 end
350 end
333
351
334 def init_journal(user, notes = "")
352 def init_journal(user, notes = "")
335 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
353 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
336 @issue_before_change = self.clone
354 @issue_before_change = self.clone
337 @issue_before_change.status = self.status
355 @issue_before_change.status = self.status
338 @custom_values_before_change = {}
356 @custom_values_before_change = {}
339 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
357 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
340 # Make sure updated_on is updated when adding a note.
358 # Make sure updated_on is updated when adding a note.
341 updated_on_will_change!
359 updated_on_will_change!
342 @current_journal
360 @current_journal
343 end
361 end
344
362
345 # Return true if the issue is closed, otherwise false
363 # Return true if the issue is closed, otherwise false
346 def closed?
364 def closed?
347 self.status.is_closed?
365 self.status.is_closed?
348 end
366 end
349
367
350 # Return true if the issue is being reopened
368 # Return true if the issue is being reopened
351 def reopened?
369 def reopened?
352 if !new_record? && status_id_changed?
370 if !new_record? && status_id_changed?
353 status_was = IssueStatus.find_by_id(status_id_was)
371 status_was = IssueStatus.find_by_id(status_id_was)
354 status_new = IssueStatus.find_by_id(status_id)
372 status_new = IssueStatus.find_by_id(status_id)
355 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
373 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
356 return true
374 return true
357 end
375 end
358 end
376 end
359 false
377 false
360 end
378 end
361
379
362 # Return true if the issue is being closed
380 # Return true if the issue is being closed
363 def closing?
381 def closing?
364 if !new_record? && status_id_changed?
382 if !new_record? && status_id_changed?
365 status_was = IssueStatus.find_by_id(status_id_was)
383 status_was = IssueStatus.find_by_id(status_id_was)
366 status_new = IssueStatus.find_by_id(status_id)
384 status_new = IssueStatus.find_by_id(status_id)
367 if status_was && status_new && !status_was.is_closed? && status_new.is_closed?
385 if status_was && status_new && !status_was.is_closed? && status_new.is_closed?
368 return true
386 return true
369 end
387 end
370 end
388 end
371 false
389 false
372 end
390 end
373
391
374 # Returns true if the issue is overdue
392 # Returns true if the issue is overdue
375 def overdue?
393 def overdue?
376 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
394 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
377 end
395 end
378
396
379 # Is the amount of work done less than it should for the due date
397 # Is the amount of work done less than it should for the due date
380 def behind_schedule?
398 def behind_schedule?
381 return false if start_date.nil? || due_date.nil?
399 return false if start_date.nil? || due_date.nil?
382 done_date = start_date + ((due_date - start_date+1)* done_ratio/100).floor
400 done_date = start_date + ((due_date - start_date+1)* done_ratio/100).floor
383 return done_date <= Date.today
401 return done_date <= Date.today
384 end
402 end
385
403
386 # Does this issue have children?
404 # Does this issue have children?
387 def children?
405 def children?
388 !leaf?
406 !leaf?
389 end
407 end
390
408
391 # Users the issue can be assigned to
409 # Users the issue can be assigned to
392 def assignable_users
410 def assignable_users
393 users = project.assignable_users
411 users = project.assignable_users
394 users << author if author
412 users << author if author
395 users.uniq.sort
413 users.uniq.sort
396 end
414 end
397
415
398 # Versions that the issue can be assigned to
416 # Versions that the issue can be assigned to
399 def assignable_versions
417 def assignable_versions
400 @assignable_versions ||= (project.shared_versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort
418 @assignable_versions ||= (project.shared_versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort
401 end
419 end
402
420
403 # Returns true if this issue is blocked by another issue that is still open
421 # Returns true if this issue is blocked by another issue that is still open
404 def blocked?
422 def blocked?
405 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
423 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
406 end
424 end
407
425
408 # Returns an array of status that user is able to apply
426 # Returns an array of status that user is able to apply
409 def new_statuses_allowed_to(user, include_default=false)
427 def new_statuses_allowed_to(user, include_default=false)
410 statuses = status.find_new_statuses_allowed_to(user.roles_for_project(project), tracker)
428 statuses = status.find_new_statuses_allowed_to(user.roles_for_project(project), tracker)
411 statuses << status unless statuses.empty?
429 statuses << status unless statuses.empty?
412 statuses << IssueStatus.default if include_default
430 statuses << IssueStatus.default if include_default
413 statuses = statuses.uniq.sort
431 statuses = statuses.uniq.sort
414 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
432 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
415 end
433 end
416
434
417 # Returns the mail adresses of users that should be notified
435 # Returns the mail adresses of users that should be notified
418 def recipients
436 def recipients
419 notified = project.notified_users
437 notified = project.notified_users
420 # Author and assignee are always notified unless they have been
438 # Author and assignee are always notified unless they have been
421 # locked or don't want to be notified
439 # locked or don't want to be notified
422 notified << author if author && author.active? && author.notify_about?(self)
440 notified << author if author && author.active? && author.notify_about?(self)
423 notified << assigned_to if assigned_to && assigned_to.active? && assigned_to.notify_about?(self)
441 notified << assigned_to if assigned_to && assigned_to.active? && assigned_to.notify_about?(self)
424 notified.uniq!
442 notified.uniq!
425 # Remove users that can not view the issue
443 # Remove users that can not view the issue
426 notified.reject! {|user| !visible?(user)}
444 notified.reject! {|user| !visible?(user)}
427 notified.collect(&:mail)
445 notified.collect(&:mail)
428 end
446 end
429
447
430 # Returns the total number of hours spent on this issue and its descendants
448 # Returns the total number of hours spent on this issue and its descendants
431 #
449 #
432 # Example:
450 # Example:
433 # spent_hours => 0.0
451 # spent_hours => 0.0
434 # spent_hours => 50.2
452 # spent_hours => 50.2
435 def spent_hours
453 def spent_hours
436 @spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours", :include => :time_entries).to_f || 0.0
454 @spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours", :include => :time_entries).to_f || 0.0
437 end
455 end
438
456
439 def relations
457 def relations
440 (relations_from + relations_to).sort
458 (relations_from + relations_to).sort
441 end
459 end
442
460
443 def all_dependent_issues
461 def all_dependent_issues
444 dependencies = []
462 dependencies = []
445 relations_from.each do |relation|
463 relations_from.each do |relation|
446 dependencies << relation.issue_to
464 dependencies << relation.issue_to
447 dependencies += relation.issue_to.all_dependent_issues
465 dependencies += relation.issue_to.all_dependent_issues
448 end
466 end
449 dependencies
467 dependencies
450 end
468 end
451
469
452 # Returns an array of issues that duplicate this one
470 # Returns an array of issues that duplicate this one
453 def duplicates
471 def duplicates
454 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
472 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
455 end
473 end
456
474
457 # Returns the due date or the target due date if any
475 # Returns the due date or the target due date if any
458 # Used on gantt chart
476 # Used on gantt chart
459 def due_before
477 def due_before
460 due_date || (fixed_version ? fixed_version.effective_date : nil)
478 due_date || (fixed_version ? fixed_version.effective_date : nil)
461 end
479 end
462
480
463 # Returns the time scheduled for this issue.
481 # Returns the time scheduled for this issue.
464 #
482 #
465 # Example:
483 # Example:
466 # Start Date: 2/26/09, End Date: 3/04/09
484 # Start Date: 2/26/09, End Date: 3/04/09
467 # duration => 6
485 # duration => 6
468 def duration
486 def duration
469 (start_date && due_date) ? due_date - start_date : 0
487 (start_date && due_date) ? due_date - start_date : 0
470 end
488 end
471
489
472 def soonest_start
490 def soonest_start
473 @soonest_start ||= (
491 @soonest_start ||= (
474 relations_to.collect{|relation| relation.successor_soonest_start} +
492 relations_to.collect{|relation| relation.successor_soonest_start} +
475 ancestors.collect(&:soonest_start)
493 ancestors.collect(&:soonest_start)
476 ).compact.max
494 ).compact.max
477 end
495 end
478
496
479 def reschedule_after(date)
497 def reschedule_after(date)
480 return if date.nil?
498 return if date.nil?
481 if leaf?
499 if leaf?
482 if start_date.nil? || start_date < date
500 if start_date.nil? || start_date < date
483 self.start_date, self.due_date = date, date + duration
501 self.start_date, self.due_date = date, date + duration
484 save
502 save
485 end
503 end
486 else
504 else
487 leaves.each do |leaf|
505 leaves.each do |leaf|
488 leaf.reschedule_after(date)
506 leaf.reschedule_after(date)
489 end
507 end
490 end
508 end
491 end
509 end
492
510
493 def <=>(issue)
511 def <=>(issue)
494 if issue.nil?
512 if issue.nil?
495 -1
513 -1
496 elsif root_id != issue.root_id
514 elsif root_id != issue.root_id
497 (root_id || 0) <=> (issue.root_id || 0)
515 (root_id || 0) <=> (issue.root_id || 0)
498 else
516 else
499 (lft || 0) <=> (issue.lft || 0)
517 (lft || 0) <=> (issue.lft || 0)
500 end
518 end
501 end
519 end
502
520
503 def to_s
521 def to_s
504 "#{tracker} ##{id}: #{subject}"
522 "#{tracker} ##{id}: #{subject}"
505 end
523 end
506
524
507 # Returns a string of css classes that apply to the issue
525 # Returns a string of css classes that apply to the issue
508 def css_classes
526 def css_classes
509 s = "issue status-#{status.position} priority-#{priority.position}"
527 s = "issue status-#{status.position} priority-#{priority.position}"
510 s << ' closed' if closed?
528 s << ' closed' if closed?
511 s << ' overdue' if overdue?
529 s << ' overdue' if overdue?
512 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
530 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
513 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
531 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
514 s
532 s
515 end
533 end
516
534
517 # Saves an issue, time_entry, attachments, and a journal from the parameters
535 # Saves an issue, time_entry, attachments, and a journal from the parameters
518 # Returns false if save fails
536 # Returns false if save fails
519 def save_issue_with_child_records(params, existing_time_entry=nil)
537 def save_issue_with_child_records(params, existing_time_entry=nil)
520 Issue.transaction do
538 Issue.transaction do
521 if params[:time_entry] && params[:time_entry][:hours].present? && User.current.allowed_to?(:log_time, project)
539 if params[:time_entry] && params[:time_entry][:hours].present? && User.current.allowed_to?(:log_time, project)
522 @time_entry = existing_time_entry || TimeEntry.new
540 @time_entry = existing_time_entry || TimeEntry.new
523 @time_entry.project = project
541 @time_entry.project = project
524 @time_entry.issue = self
542 @time_entry.issue = self
525 @time_entry.user = User.current
543 @time_entry.user = User.current
526 @time_entry.spent_on = Date.today
544 @time_entry.spent_on = Date.today
527 @time_entry.attributes = params[:time_entry]
545 @time_entry.attributes = params[:time_entry]
528 self.time_entries << @time_entry
546 self.time_entries << @time_entry
529 end
547 end
530
548
531 if valid?
549 if valid?
532 attachments = Attachment.attach_files(self, params[:attachments])
550 attachments = Attachment.attach_files(self, params[:attachments])
533
551
534 attachments[:files].each {|a| @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
552 attachments[:files].each {|a| @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
535 # TODO: Rename hook
553 # TODO: Rename hook
536 Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
554 Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
537 begin
555 begin
538 if save
556 if save
539 # TODO: Rename hook
557 # TODO: Rename hook
540 Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
558 Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
541 else
559 else
542 raise ActiveRecord::Rollback
560 raise ActiveRecord::Rollback
543 end
561 end
544 rescue ActiveRecord::StaleObjectError
562 rescue ActiveRecord::StaleObjectError
545 attachments[:files].each(&:destroy)
563 attachments[:files].each(&:destroy)
546 errors.add_to_base l(:notice_locking_conflict)
564 errors.add_to_base l(:notice_locking_conflict)
547 raise ActiveRecord::Rollback
565 raise ActiveRecord::Rollback
548 end
566 end
549 end
567 end
550 end
568 end
551 end
569 end
552
570
553 # Unassigns issues from +version+ if it's no longer shared with issue's project
571 # Unassigns issues from +version+ if it's no longer shared with issue's project
554 def self.update_versions_from_sharing_change(version)
572 def self.update_versions_from_sharing_change(version)
555 # Update issues assigned to the version
573 # Update issues assigned to the version
556 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
574 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
557 end
575 end
558
576
559 # Unassigns issues from versions that are no longer shared
577 # Unassigns issues from versions that are no longer shared
560 # after +project+ was moved
578 # after +project+ was moved
561 def self.update_versions_from_hierarchy_change(project)
579 def self.update_versions_from_hierarchy_change(project)
562 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
580 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
563 # Update issues of the moved projects and issues assigned to a version of a moved project
581 # Update issues of the moved projects and issues assigned to a version of a moved project
564 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
582 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
565 end
583 end
566
584
567 def parent_issue_id=(arg)
585 def parent_issue_id=(arg)
568 parent_issue_id = arg.blank? ? nil : arg.to_i
586 parent_issue_id = arg.blank? ? nil : arg.to_i
569 if parent_issue_id && @parent_issue = Issue.find_by_id(parent_issue_id)
587 if parent_issue_id && @parent_issue = Issue.find_by_id(parent_issue_id)
570 @parent_issue.id
588 @parent_issue.id
571 else
589 else
572 @parent_issue = nil
590 @parent_issue = nil
573 nil
591 nil
574 end
592 end
575 end
593 end
576
594
577 def parent_issue_id
595 def parent_issue_id
578 if instance_variable_defined? :@parent_issue
596 if instance_variable_defined? :@parent_issue
579 @parent_issue.nil? ? nil : @parent_issue.id
597 @parent_issue.nil? ? nil : @parent_issue.id
580 else
598 else
581 parent_id
599 parent_id
582 end
600 end
583 end
601 end
584
602
585 # Extracted from the ReportsController.
603 # Extracted from the ReportsController.
586 def self.by_tracker(project)
604 def self.by_tracker(project)
587 count_and_group_by(:project => project,
605 count_and_group_by(:project => project,
588 :field => 'tracker_id',
606 :field => 'tracker_id',
589 :joins => Tracker.table_name)
607 :joins => Tracker.table_name)
590 end
608 end
591
609
592 def self.by_version(project)
610 def self.by_version(project)
593 count_and_group_by(:project => project,
611 count_and_group_by(:project => project,
594 :field => 'fixed_version_id',
612 :field => 'fixed_version_id',
595 :joins => Version.table_name)
613 :joins => Version.table_name)
596 end
614 end
597
615
598 def self.by_priority(project)
616 def self.by_priority(project)
599 count_and_group_by(:project => project,
617 count_and_group_by(:project => project,
600 :field => 'priority_id',
618 :field => 'priority_id',
601 :joins => IssuePriority.table_name)
619 :joins => IssuePriority.table_name)
602 end
620 end
603
621
604 def self.by_category(project)
622 def self.by_category(project)
605 count_and_group_by(:project => project,
623 count_and_group_by(:project => project,
606 :field => 'category_id',
624 :field => 'category_id',
607 :joins => IssueCategory.table_name)
625 :joins => IssueCategory.table_name)
608 end
626 end
609
627
610 def self.by_assigned_to(project)
628 def self.by_assigned_to(project)
611 count_and_group_by(:project => project,
629 count_and_group_by(:project => project,
612 :field => 'assigned_to_id',
630 :field => 'assigned_to_id',
613 :joins => User.table_name)
631 :joins => User.table_name)
614 end
632 end
615
633
616 def self.by_author(project)
634 def self.by_author(project)
617 count_and_group_by(:project => project,
635 count_and_group_by(:project => project,
618 :field => 'author_id',
636 :field => 'author_id',
619 :joins => User.table_name)
637 :joins => User.table_name)
620 end
638 end
621
639
622 def self.by_subproject(project)
640 def self.by_subproject(project)
623 ActiveRecord::Base.connection.select_all("select s.id as status_id,
641 ActiveRecord::Base.connection.select_all("select s.id as status_id,
624 s.is_closed as closed,
642 s.is_closed as closed,
625 i.project_id as project_id,
643 i.project_id as project_id,
626 count(i.id) as total
644 count(i.id) as total
627 from
645 from
628 #{Issue.table_name} i, #{IssueStatus.table_name} s
646 #{Issue.table_name} i, #{IssueStatus.table_name} s
629 where
647 where
630 i.status_id=s.id
648 i.status_id=s.id
631 and i.project_id IN (#{project.descendants.active.collect{|p| p.id}.join(',')})
649 and i.project_id IN (#{project.descendants.active.collect{|p| p.id}.join(',')})
632 group by s.id, s.is_closed, i.project_id") if project.descendants.active.any?
650 group by s.id, s.is_closed, i.project_id") if project.descendants.active.any?
633 end
651 end
634 # End ReportsController extraction
652 # End ReportsController extraction
635
653
636 # Returns an array of projects that current user can move issues to
654 # Returns an array of projects that current user can move issues to
637 def self.allowed_target_projects_on_move
655 def self.allowed_target_projects_on_move
638 projects = []
656 projects = []
639 if User.current.admin?
657 if User.current.admin?
640 # admin is allowed to move issues to any active (visible) project
658 # admin is allowed to move issues to any active (visible) project
641 projects = Project.visible.all
659 projects = Project.visible.all
642 elsif User.current.logged?
660 elsif User.current.logged?
643 if Role.non_member.allowed_to?(:move_issues)
661 if Role.non_member.allowed_to?(:move_issues)
644 projects = Project.visible.all
662 projects = Project.visible.all
645 else
663 else
646 User.current.memberships.each {|m| projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}}
664 User.current.memberships.each {|m| projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}}
647 end
665 end
648 end
666 end
649 projects
667 projects
650 end
668 end
651
669
652 private
670 private
653
671
654 def update_nested_set_attributes
672 def update_nested_set_attributes
655 if root_id.nil?
673 if root_id.nil?
656 # issue was just created
674 # issue was just created
657 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
675 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
658 set_default_left_and_right
676 set_default_left_and_right
659 Issue.update_all("root_id = #{root_id}, lft = #{lft}, rgt = #{rgt}", ["id = ?", id])
677 Issue.update_all("root_id = #{root_id}, lft = #{lft}, rgt = #{rgt}", ["id = ?", id])
660 if @parent_issue
678 if @parent_issue
661 move_to_child_of(@parent_issue)
679 move_to_child_of(@parent_issue)
662 end
680 end
663 reload
681 reload
664 elsif parent_issue_id != parent_id
682 elsif parent_issue_id != parent_id
665 former_parent_id = parent_id
683 former_parent_id = parent_id
666 # moving an existing issue
684 # moving an existing issue
667 if @parent_issue && @parent_issue.root_id == root_id
685 if @parent_issue && @parent_issue.root_id == root_id
668 # inside the same tree
686 # inside the same tree
669 move_to_child_of(@parent_issue)
687 move_to_child_of(@parent_issue)
670 else
688 else
671 # to another tree
689 # to another tree
672 unless root?
690 unless root?
673 move_to_right_of(root)
691 move_to_right_of(root)
674 reload
692 reload
675 end
693 end
676 old_root_id = root_id
694 old_root_id = root_id
677 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
695 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
678 target_maxright = nested_set_scope.maximum(right_column_name) || 0
696 target_maxright = nested_set_scope.maximum(right_column_name) || 0
679 offset = target_maxright + 1 - lft
697 offset = target_maxright + 1 - lft
680 Issue.update_all("root_id = #{root_id}, lft = lft + #{offset}, rgt = rgt + #{offset}",
698 Issue.update_all("root_id = #{root_id}, lft = lft + #{offset}, rgt = rgt + #{offset}",
681 ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
699 ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
682 self[left_column_name] = lft + offset
700 self[left_column_name] = lft + offset
683 self[right_column_name] = rgt + offset
701 self[right_column_name] = rgt + offset
684 if @parent_issue
702 if @parent_issue
685 move_to_child_of(@parent_issue)
703 move_to_child_of(@parent_issue)
686 end
704 end
687 end
705 end
688 reload
706 reload
689 # delete invalid relations of all descendants
707 # delete invalid relations of all descendants
690 self_and_descendants.each do |issue|
708 self_and_descendants.each do |issue|
691 issue.relations.each do |relation|
709 issue.relations.each do |relation|
692 relation.destroy unless relation.valid?
710 relation.destroy unless relation.valid?
693 end
711 end
694 end
712 end
695 # update former parent
713 # update former parent
696 recalculate_attributes_for(former_parent_id) if former_parent_id
714 recalculate_attributes_for(former_parent_id) if former_parent_id
697 end
715 end
698 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
716 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
699 end
717 end
700
718
701 def update_parent_attributes
719 def update_parent_attributes
702 recalculate_attributes_for(parent_id) if parent_id
720 recalculate_attributes_for(parent_id) if parent_id
703 end
721 end
704
722
705 def recalculate_attributes_for(issue_id)
723 def recalculate_attributes_for(issue_id)
706 if issue_id && p = Issue.find_by_id(issue_id)
724 if issue_id && p = Issue.find_by_id(issue_id)
707 # priority = highest priority of children
725 # priority = highest priority of children
708 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :include => :priority)
726 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :include => :priority)
709 p.priority = IssuePriority.find_by_position(priority_position)
727 p.priority = IssuePriority.find_by_position(priority_position)
710 end
728 end
711
729
712 # start/due dates = lowest/highest dates of children
730 # start/due dates = lowest/highest dates of children
713 p.start_date = p.children.minimum(:start_date)
731 p.start_date = p.children.minimum(:start_date)
714 p.due_date = p.children.maximum(:due_date)
732 p.due_date = p.children.maximum(:due_date)
715 if p.start_date && p.due_date && p.due_date < p.start_date
733 if p.start_date && p.due_date && p.due_date < p.start_date
716 p.start_date, p.due_date = p.due_date, p.start_date
734 p.start_date, p.due_date = p.due_date, p.start_date
717 end
735 end
718
736
719 # done ratio = weighted average ratio of leaves
737 # done ratio = weighted average ratio of leaves
720 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
738 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
721 leaves_count = p.leaves.count
739 leaves_count = p.leaves.count
722 if leaves_count > 0
740 if leaves_count > 0
723 average = p.leaves.average(:estimated_hours).to_f
741 average = p.leaves.average(:estimated_hours).to_f
724 if average == 0
742 if average == 0
725 average = 1
743 average = 1
726 end
744 end
727 done = p.leaves.sum("COALESCE(estimated_hours, #{average}) * (CASE WHEN is_closed = #{connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)", :include => :status).to_f
745 done = p.leaves.sum("COALESCE(estimated_hours, #{average}) * (CASE WHEN is_closed = #{connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)", :include => :status).to_f
728 progress = done / (average * leaves_count)
746 progress = done / (average * leaves_count)
729 p.done_ratio = progress.round
747 p.done_ratio = progress.round
730 end
748 end
731 end
749 end
732
750
733 # estimate = sum of leaves estimates
751 # estimate = sum of leaves estimates
734 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
752 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
735 p.estimated_hours = nil if p.estimated_hours == 0.0
753 p.estimated_hours = nil if p.estimated_hours == 0.0
736
754
737 # ancestors will be recursively updated
755 # ancestors will be recursively updated
738 p.save(false)
756 p.save(false)
739 end
757 end
740 end
758 end
741
759
742 def destroy_children
760 def destroy_children
743 unless leaf?
761 unless leaf?
744 children.each do |child|
762 children.each do |child|
745 child.destroy
763 child.destroy
746 end
764 end
747 end
765 end
748 end
766 end
749
767
750 # Update issues so their versions are not pointing to a
768 # Update issues so their versions are not pointing to a
751 # fixed_version that is not shared with the issue's project
769 # fixed_version that is not shared with the issue's project
752 def self.update_versions(conditions=nil)
770 def self.update_versions(conditions=nil)
753 # Only need to update issues with a fixed_version from
771 # Only need to update issues with a fixed_version from
754 # a different project and that is not systemwide shared
772 # a different project and that is not systemwide shared
755 Issue.all(:conditions => merge_conditions("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
773 Issue.all(:conditions => merge_conditions("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
756 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
774 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
757 " AND #{Version.table_name}.sharing <> 'system'",
775 " AND #{Version.table_name}.sharing <> 'system'",
758 conditions),
776 conditions),
759 :include => [:project, :fixed_version]
777 :include => [:project, :fixed_version]
760 ).each do |issue|
778 ).each do |issue|
761 next if issue.project.nil? || issue.fixed_version.nil?
779 next if issue.project.nil? || issue.fixed_version.nil?
762 unless issue.project.shared_versions.include?(issue.fixed_version)
780 unless issue.project.shared_versions.include?(issue.fixed_version)
763 issue.init_journal(User.current)
781 issue.init_journal(User.current)
764 issue.fixed_version = nil
782 issue.fixed_version = nil
765 issue.save
783 issue.save
766 end
784 end
767 end
785 end
768 end
786 end
769
787
770 # Callback on attachment deletion
788 # Callback on attachment deletion
771 def attachment_removed(obj)
789 def attachment_removed(obj)
772 journal = init_journal(User.current)
790 journal = init_journal(User.current)
773 journal.details << JournalDetail.new(:property => 'attachment',
791 journal.details << JournalDetail.new(:property => 'attachment',
774 :prop_key => obj.id,
792 :prop_key => obj.id,
775 :old_value => obj.filename)
793 :old_value => obj.filename)
776 journal.save
794 journal.save
777 end
795 end
778
796
779 # Default assignment based on category
797 # Default assignment based on category
780 def default_assign
798 def default_assign
781 if assigned_to.nil? && category && category.assigned_to
799 if assigned_to.nil? && category && category.assigned_to
782 self.assigned_to = category.assigned_to
800 self.assigned_to = category.assigned_to
783 end
801 end
784 end
802 end
785
803
786 # Updates start/due dates of following issues
804 # Updates start/due dates of following issues
787 def reschedule_following_issues
805 def reschedule_following_issues
788 if start_date_changed? || due_date_changed?
806 if start_date_changed? || due_date_changed?
789 relations_from.each do |relation|
807 relations_from.each do |relation|
790 relation.set_issue_to_dates
808 relation.set_issue_to_dates
791 end
809 end
792 end
810 end
793 end
811 end
794
812
795 # Closes duplicates if the issue is being closed
813 # Closes duplicates if the issue is being closed
796 def close_duplicates
814 def close_duplicates
797 if closing?
815 if closing?
798 duplicates.each do |duplicate|
816 duplicates.each do |duplicate|
799 # Reload is need in case the duplicate was updated by a previous duplicate
817 # Reload is need in case the duplicate was updated by a previous duplicate
800 duplicate.reload
818 duplicate.reload
801 # Don't re-close it if it's already closed
819 # Don't re-close it if it's already closed
802 next if duplicate.closed?
820 next if duplicate.closed?
803 # Same user and notes
821 # Same user and notes
804 if @current_journal
822 if @current_journal
805 duplicate.init_journal(@current_journal.user, @current_journal.notes)
823 duplicate.init_journal(@current_journal.user, @current_journal.notes)
806 end
824 end
807 duplicate.update_attribute :status, self.status
825 duplicate.update_attribute :status, self.status
808 end
826 end
809 end
827 end
810 end
828 end
811
829
812 # Saves the changes in a Journal
830 # Saves the changes in a Journal
813 # Called after_save
831 # Called after_save
814 def create_journal
832 def create_journal
815 if @current_journal
833 if @current_journal
816 # attributes changes
834 # attributes changes
817 (Issue.column_names - %w(id description root_id lft rgt lock_version created_on updated_on)).each {|c|
835 (Issue.column_names - %w(id description root_id lft rgt lock_version created_on updated_on)).each {|c|
818 @current_journal.details << JournalDetail.new(:property => 'attr',
836 @current_journal.details << JournalDetail.new(:property => 'attr',
819 :prop_key => c,
837 :prop_key => c,
820 :old_value => @issue_before_change.send(c),
838 :old_value => @issue_before_change.send(c),
821 :value => send(c)) unless send(c)==@issue_before_change.send(c)
839 :value => send(c)) unless send(c)==@issue_before_change.send(c)
822 }
840 }
823 # custom fields changes
841 # custom fields changes
824 custom_values.each {|c|
842 custom_values.each {|c|
825 next if (@custom_values_before_change[c.custom_field_id]==c.value ||
843 next if (@custom_values_before_change[c.custom_field_id]==c.value ||
826 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
844 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
827 @current_journal.details << JournalDetail.new(:property => 'cf',
845 @current_journal.details << JournalDetail.new(:property => 'cf',
828 :prop_key => c.custom_field_id,
846 :prop_key => c.custom_field_id,
829 :old_value => @custom_values_before_change[c.custom_field_id],
847 :old_value => @custom_values_before_change[c.custom_field_id],
830 :value => c.value)
848 :value => c.value)
831 }
849 }
832 @current_journal.save
850 @current_journal.save
833 # reset current journal
851 # reset current journal
834 init_journal @current_journal.user, @current_journal.notes
852 init_journal @current_journal.user, @current_journal.notes
835 end
853 end
836 end
854 end
837
855
838 # Query generator for selecting groups of issue counts for a project
856 # Query generator for selecting groups of issue counts for a project
839 # based on specific criteria
857 # based on specific criteria
840 #
858 #
841 # Options
859 # Options
842 # * project - Project to search in.
860 # * project - Project to search in.
843 # * field - String. Issue field to key off of in the grouping.
861 # * field - String. Issue field to key off of in the grouping.
844 # * joins - String. The table name to join against.
862 # * joins - String. The table name to join against.
845 def self.count_and_group_by(options)
863 def self.count_and_group_by(options)
846 project = options.delete(:project)
864 project = options.delete(:project)
847 select_field = options.delete(:field)
865 select_field = options.delete(:field)
848 joins = options.delete(:joins)
866 joins = options.delete(:joins)
849
867
850 where = "i.#{select_field}=j.id"
868 where = "i.#{select_field}=j.id"
851
869
852 ActiveRecord::Base.connection.select_all("select s.id as status_id,
870 ActiveRecord::Base.connection.select_all("select s.id as status_id,
853 s.is_closed as closed,
871 s.is_closed as closed,
854 j.id as #{select_field},
872 j.id as #{select_field},
855 count(i.id) as total
873 count(i.id) as total
856 from
874 from
857 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{joins} j
875 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{joins} j
858 where
876 where
859 i.status_id=s.id
877 i.status_id=s.id
860 and #{where}
878 and #{where}
861 and i.project_id=#{project.id}
879 and i.project_id=#{project.id}
862 group by s.id, s.is_closed, j.id")
880 group by s.id, s.is_closed, j.id")
863 end
881 end
864
882
865
883
866 end
884 end
@@ -1,1172 +1,1271
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 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.dirname(__FILE__) + '/../test_helper'
18 require File.dirname(__FILE__) + '/../test_helper'
19 require 'issues_controller'
19 require 'issues_controller'
20
20
21 # Re-raise errors caught by the controller.
21 # Re-raise errors caught by the controller.
22 class IssuesController; def rescue_action(e) raise e end; end
22 class IssuesController; def rescue_action(e) raise e end; end
23
23
24 class IssuesControllerTest < ActionController::TestCase
24 class IssuesControllerTest < ActionController::TestCase
25 fixtures :projects,
25 fixtures :projects,
26 :users,
26 :users,
27 :roles,
27 :roles,
28 :members,
28 :members,
29 :member_roles,
29 :member_roles,
30 :issues,
30 :issues,
31 :issue_statuses,
31 :issue_statuses,
32 :versions,
32 :versions,
33 :trackers,
33 :trackers,
34 :projects_trackers,
34 :projects_trackers,
35 :issue_categories,
35 :issue_categories,
36 :enabled_modules,
36 :enabled_modules,
37 :enumerations,
37 :enumerations,
38 :attachments,
38 :attachments,
39 :workflows,
39 :workflows,
40 :custom_fields,
40 :custom_fields,
41 :custom_values,
41 :custom_values,
42 :custom_fields_projects,
42 :custom_fields_projects,
43 :custom_fields_trackers,
43 :custom_fields_trackers,
44 :time_entries,
44 :time_entries,
45 :journals,
45 :journals,
46 :journal_details,
46 :journal_details,
47 :queries
47 :queries
48
48
49 def setup
49 def setup
50 @controller = IssuesController.new
50 @controller = IssuesController.new
51 @request = ActionController::TestRequest.new
51 @request = ActionController::TestRequest.new
52 @response = ActionController::TestResponse.new
52 @response = ActionController::TestResponse.new
53 User.current = nil
53 User.current = nil
54 end
54 end
55
55
56 def test_index
56 def test_index
57 Setting.default_language = 'en'
57 Setting.default_language = 'en'
58
58
59 get :index
59 get :index
60 assert_response :success
60 assert_response :success
61 assert_template 'index.rhtml'
61 assert_template 'index.rhtml'
62 assert_not_nil assigns(:issues)
62 assert_not_nil assigns(:issues)
63 assert_nil assigns(:project)
63 assert_nil assigns(:project)
64 assert_tag :tag => 'a', :content => /Can't print recipes/
64 assert_tag :tag => 'a', :content => /Can't print recipes/
65 assert_tag :tag => 'a', :content => /Subproject issue/
65 assert_tag :tag => 'a', :content => /Subproject issue/
66 # private projects hidden
66 # private projects hidden
67 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
67 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
68 assert_no_tag :tag => 'a', :content => /Issue on project 2/
68 assert_no_tag :tag => 'a', :content => /Issue on project 2/
69 # project column
69 # project column
70 assert_tag :tag => 'th', :content => /Project/
70 assert_tag :tag => 'th', :content => /Project/
71 end
71 end
72
72
73 def test_index_should_not_list_issues_when_module_disabled
73 def test_index_should_not_list_issues_when_module_disabled
74 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
74 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
75 get :index
75 get :index
76 assert_response :success
76 assert_response :success
77 assert_template 'index.rhtml'
77 assert_template 'index.rhtml'
78 assert_not_nil assigns(:issues)
78 assert_not_nil assigns(:issues)
79 assert_nil assigns(:project)
79 assert_nil assigns(:project)
80 assert_no_tag :tag => 'a', :content => /Can't print recipes/
80 assert_no_tag :tag => 'a', :content => /Can't print recipes/
81 assert_tag :tag => 'a', :content => /Subproject issue/
81 assert_tag :tag => 'a', :content => /Subproject issue/
82 end
82 end
83
83
84 def test_index_should_not_list_issues_when_module_disabled
84 def test_index_should_not_list_issues_when_module_disabled
85 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
85 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
86 get :index
86 get :index
87 assert_response :success
87 assert_response :success
88 assert_template 'index.rhtml'
88 assert_template 'index.rhtml'
89 assert_not_nil assigns(:issues)
89 assert_not_nil assigns(:issues)
90 assert_nil assigns(:project)
90 assert_nil assigns(:project)
91 assert_no_tag :tag => 'a', :content => /Can't print recipes/
91 assert_no_tag :tag => 'a', :content => /Can't print recipes/
92 assert_tag :tag => 'a', :content => /Subproject issue/
92 assert_tag :tag => 'a', :content => /Subproject issue/
93 end
93 end
94
94
95 def test_index_with_project
95 def test_index_with_project
96 Setting.display_subprojects_issues = 0
96 Setting.display_subprojects_issues = 0
97 get :index, :project_id => 1
97 get :index, :project_id => 1
98 assert_response :success
98 assert_response :success
99 assert_template 'index.rhtml'
99 assert_template 'index.rhtml'
100 assert_not_nil assigns(:issues)
100 assert_not_nil assigns(:issues)
101 assert_tag :tag => 'a', :content => /Can't print recipes/
101 assert_tag :tag => 'a', :content => /Can't print recipes/
102 assert_no_tag :tag => 'a', :content => /Subproject issue/
102 assert_no_tag :tag => 'a', :content => /Subproject issue/
103 end
103 end
104
104
105 def test_index_with_project_and_subprojects
105 def test_index_with_project_and_subprojects
106 Setting.display_subprojects_issues = 1
106 Setting.display_subprojects_issues = 1
107 get :index, :project_id => 1
107 get :index, :project_id => 1
108 assert_response :success
108 assert_response :success
109 assert_template 'index.rhtml'
109 assert_template 'index.rhtml'
110 assert_not_nil assigns(:issues)
110 assert_not_nil assigns(:issues)
111 assert_tag :tag => 'a', :content => /Can't print recipes/
111 assert_tag :tag => 'a', :content => /Can't print recipes/
112 assert_tag :tag => 'a', :content => /Subproject issue/
112 assert_tag :tag => 'a', :content => /Subproject issue/
113 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
113 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
114 end
114 end
115
115
116 def test_index_with_project_and_subprojects_should_show_private_subprojects
116 def test_index_with_project_and_subprojects_should_show_private_subprojects
117 @request.session[:user_id] = 2
117 @request.session[:user_id] = 2
118 Setting.display_subprojects_issues = 1
118 Setting.display_subprojects_issues = 1
119 get :index, :project_id => 1
119 get :index, :project_id => 1
120 assert_response :success
120 assert_response :success
121 assert_template 'index.rhtml'
121 assert_template 'index.rhtml'
122 assert_not_nil assigns(:issues)
122 assert_not_nil assigns(:issues)
123 assert_tag :tag => 'a', :content => /Can't print recipes/
123 assert_tag :tag => 'a', :content => /Can't print recipes/
124 assert_tag :tag => 'a', :content => /Subproject issue/
124 assert_tag :tag => 'a', :content => /Subproject issue/
125 assert_tag :tag => 'a', :content => /Issue of a private subproject/
125 assert_tag :tag => 'a', :content => /Issue of a private subproject/
126 end
126 end
127
127
128 def test_index_with_project_and_default_filter
128 def test_index_with_project_and_default_filter
129 get :index, :project_id => 1, :set_filter => 1
129 get :index, :project_id => 1, :set_filter => 1
130 assert_response :success
130 assert_response :success
131 assert_template 'index.rhtml'
131 assert_template 'index.rhtml'
132 assert_not_nil assigns(:issues)
132 assert_not_nil assigns(:issues)
133
133
134 query = assigns(:query)
134 query = assigns(:query)
135 assert_not_nil query
135 assert_not_nil query
136 # default filter
136 # default filter
137 assert_equal({'status_id' => {:operator => 'o', :values => ['']}}, query.filters)
137 assert_equal({'status_id' => {:operator => 'o', :values => ['']}}, query.filters)
138 end
138 end
139
139
140 def test_index_with_project_and_filter
140 def test_index_with_project_and_filter
141 get :index, :project_id => 1, :set_filter => 1,
141 get :index, :project_id => 1, :set_filter => 1,
142 :fields => ['tracker_id'],
142 :fields => ['tracker_id'],
143 :operators => {'tracker_id' => '='},
143 :operators => {'tracker_id' => '='},
144 :values => {'tracker_id' => ['1']}
144 :values => {'tracker_id' => ['1']}
145 assert_response :success
145 assert_response :success
146 assert_template 'index.rhtml'
146 assert_template 'index.rhtml'
147 assert_not_nil assigns(:issues)
147 assert_not_nil assigns(:issues)
148
148
149 query = assigns(:query)
149 query = assigns(:query)
150 assert_not_nil query
150 assert_not_nil query
151 assert_equal({'tracker_id' => {:operator => '=', :values => ['1']}}, query.filters)
151 assert_equal({'tracker_id' => {:operator => '=', :values => ['1']}}, query.filters)
152 end
152 end
153
153
154 def test_index_with_project_and_empty_filters
154 def test_index_with_project_and_empty_filters
155 get :index, :project_id => 1, :set_filter => 1, :fields => ['']
155 get :index, :project_id => 1, :set_filter => 1, :fields => ['']
156 assert_response :success
156 assert_response :success
157 assert_template 'index.rhtml'
157 assert_template 'index.rhtml'
158 assert_not_nil assigns(:issues)
158 assert_not_nil assigns(:issues)
159
159
160 query = assigns(:query)
160 query = assigns(:query)
161 assert_not_nil query
161 assert_not_nil query
162 # no filter
162 # no filter
163 assert_equal({}, query.filters)
163 assert_equal({}, query.filters)
164 end
164 end
165
165
166 def test_index_with_query
166 def test_index_with_query
167 get :index, :project_id => 1, :query_id => 5
167 get :index, :project_id => 1, :query_id => 5
168 assert_response :success
168 assert_response :success
169 assert_template 'index.rhtml'
169 assert_template 'index.rhtml'
170 assert_not_nil assigns(:issues)
170 assert_not_nil assigns(:issues)
171 assert_nil assigns(:issue_count_by_group)
171 assert_nil assigns(:issue_count_by_group)
172 end
172 end
173
173
174 def test_index_with_query_grouped_by_tracker
174 def test_index_with_query_grouped_by_tracker
175 get :index, :project_id => 1, :query_id => 6
175 get :index, :project_id => 1, :query_id => 6
176 assert_response :success
176 assert_response :success
177 assert_template 'index.rhtml'
177 assert_template 'index.rhtml'
178 assert_not_nil assigns(:issues)
178 assert_not_nil assigns(:issues)
179 assert_not_nil assigns(:issue_count_by_group)
179 assert_not_nil assigns(:issue_count_by_group)
180 end
180 end
181
181
182 def test_index_with_query_grouped_by_list_custom_field
182 def test_index_with_query_grouped_by_list_custom_field
183 get :index, :project_id => 1, :query_id => 9
183 get :index, :project_id => 1, :query_id => 9
184 assert_response :success
184 assert_response :success
185 assert_template 'index.rhtml'
185 assert_template 'index.rhtml'
186 assert_not_nil assigns(:issues)
186 assert_not_nil assigns(:issues)
187 assert_not_nil assigns(:issue_count_by_group)
187 assert_not_nil assigns(:issue_count_by_group)
188 end
188 end
189
189
190 def test_index_sort_by_field_not_included_in_columns
190 def test_index_sort_by_field_not_included_in_columns
191 Setting.issue_list_default_columns = %w(subject author)
191 Setting.issue_list_default_columns = %w(subject author)
192 get :index, :sort => 'tracker'
192 get :index, :sort => 'tracker'
193 end
193 end
194
194
195 def test_index_csv_with_project
195 def test_index_csv_with_project
196 Setting.default_language = 'en'
196 Setting.default_language = 'en'
197
197
198 get :index, :format => 'csv'
198 get :index, :format => 'csv'
199 assert_response :success
199 assert_response :success
200 assert_not_nil assigns(:issues)
200 assert_not_nil assigns(:issues)
201 assert_equal 'text/csv', @response.content_type
201 assert_equal 'text/csv', @response.content_type
202 assert @response.body.starts_with?("#,")
202 assert @response.body.starts_with?("#,")
203
203
204 get :index, :project_id => 1, :format => 'csv'
204 get :index, :project_id => 1, :format => 'csv'
205 assert_response :success
205 assert_response :success
206 assert_not_nil assigns(:issues)
206 assert_not_nil assigns(:issues)
207 assert_equal 'text/csv', @response.content_type
207 assert_equal 'text/csv', @response.content_type
208 end
208 end
209
209
210 def test_index_pdf
210 def test_index_pdf
211 get :index, :format => 'pdf'
211 get :index, :format => 'pdf'
212 assert_response :success
212 assert_response :success
213 assert_not_nil assigns(:issues)
213 assert_not_nil assigns(:issues)
214 assert_equal 'application/pdf', @response.content_type
214 assert_equal 'application/pdf', @response.content_type
215
215
216 get :index, :project_id => 1, :format => 'pdf'
216 get :index, :project_id => 1, :format => 'pdf'
217 assert_response :success
217 assert_response :success
218 assert_not_nil assigns(:issues)
218 assert_not_nil assigns(:issues)
219 assert_equal 'application/pdf', @response.content_type
219 assert_equal 'application/pdf', @response.content_type
220
220
221 get :index, :project_id => 1, :query_id => 6, :format => 'pdf'
221 get :index, :project_id => 1, :query_id => 6, :format => 'pdf'
222 assert_response :success
222 assert_response :success
223 assert_not_nil assigns(:issues)
223 assert_not_nil assigns(:issues)
224 assert_equal 'application/pdf', @response.content_type
224 assert_equal 'application/pdf', @response.content_type
225 end
225 end
226
226
227 def test_index_pdf_with_query_grouped_by_list_custom_field
227 def test_index_pdf_with_query_grouped_by_list_custom_field
228 get :index, :project_id => 1, :query_id => 9, :format => 'pdf'
228 get :index, :project_id => 1, :query_id => 9, :format => 'pdf'
229 assert_response :success
229 assert_response :success
230 assert_not_nil assigns(:issues)
230 assert_not_nil assigns(:issues)
231 assert_not_nil assigns(:issue_count_by_group)
231 assert_not_nil assigns(:issue_count_by_group)
232 assert_equal 'application/pdf', @response.content_type
232 assert_equal 'application/pdf', @response.content_type
233 end
233 end
234
234
235 def test_index_sort
235 def test_index_sort
236 get :index, :sort => 'tracker,id:desc'
236 get :index, :sort => 'tracker,id:desc'
237 assert_response :success
237 assert_response :success
238
238
239 sort_params = @request.session['issues_index_sort']
239 sort_params = @request.session['issues_index_sort']
240 assert sort_params.is_a?(String)
240 assert sort_params.is_a?(String)
241 assert_equal 'tracker,id:desc', sort_params
241 assert_equal 'tracker,id:desc', sort_params
242
242
243 issues = assigns(:issues)
243 issues = assigns(:issues)
244 assert_not_nil issues
244 assert_not_nil issues
245 assert !issues.empty?
245 assert !issues.empty?
246 assert_equal issues.sort {|a,b| a.tracker == b.tracker ? b.id <=> a.id : a.tracker <=> b.tracker }.collect(&:id), issues.collect(&:id)
246 assert_equal issues.sort {|a,b| a.tracker == b.tracker ? b.id <=> a.id : a.tracker <=> b.tracker }.collect(&:id), issues.collect(&:id)
247 end
247 end
248
248
249 def test_index_with_columns
249 def test_index_with_columns
250 columns = ['tracker', 'subject', 'assigned_to']
250 columns = ['tracker', 'subject', 'assigned_to']
251 get :index, :set_filter => 1, :query => { 'column_names' => columns}
251 get :index, :set_filter => 1, :query => { 'column_names' => columns}
252 assert_response :success
252 assert_response :success
253
253
254 # query should use specified columns
254 # query should use specified columns
255 query = assigns(:query)
255 query = assigns(:query)
256 assert_kind_of Query, query
256 assert_kind_of Query, query
257 assert_equal columns, query.column_names.map(&:to_s)
257 assert_equal columns, query.column_names.map(&:to_s)
258
258
259 # columns should be stored in session
259 # columns should be stored in session
260 assert_kind_of Hash, session[:query]
260 assert_kind_of Hash, session[:query]
261 assert_kind_of Array, session[:query][:column_names]
261 assert_kind_of Array, session[:query][:column_names]
262 assert_equal columns, session[:query][:column_names].map(&:to_s)
262 assert_equal columns, session[:query][:column_names].map(&:to_s)
263 end
263 end
264
264
265 def test_show_by_anonymous
265 def test_show_by_anonymous
266 get :show, :id => 1
266 get :show, :id => 1
267 assert_response :success
267 assert_response :success
268 assert_template 'show.rhtml'
268 assert_template 'show.rhtml'
269 assert_not_nil assigns(:issue)
269 assert_not_nil assigns(:issue)
270 assert_equal Issue.find(1), assigns(:issue)
270 assert_equal Issue.find(1), assigns(:issue)
271
271
272 # anonymous role is allowed to add a note
272 # anonymous role is allowed to add a note
273 assert_tag :tag => 'form',
273 assert_tag :tag => 'form',
274 :descendant => { :tag => 'fieldset',
274 :descendant => { :tag => 'fieldset',
275 :child => { :tag => 'legend',
275 :child => { :tag => 'legend',
276 :content => /Notes/ } }
276 :content => /Notes/ } }
277 end
277 end
278
278
279 def test_show_by_manager
279 def test_show_by_manager
280 @request.session[:user_id] = 2
280 @request.session[:user_id] = 2
281 get :show, :id => 1
281 get :show, :id => 1
282 assert_response :success
282 assert_response :success
283
283
284 assert_tag :tag => 'form',
284 assert_tag :tag => 'form',
285 :descendant => { :tag => 'fieldset',
285 :descendant => { :tag => 'fieldset',
286 :child => { :tag => 'legend',
286 :child => { :tag => 'legend',
287 :content => /Change properties/ } },
287 :content => /Change properties/ } },
288 :descendant => { :tag => 'fieldset',
288 :descendant => { :tag => 'fieldset',
289 :child => { :tag => 'legend',
289 :child => { :tag => 'legend',
290 :content => /Log time/ } },
290 :content => /Log time/ } },
291 :descendant => { :tag => 'fieldset',
291 :descendant => { :tag => 'fieldset',
292 :child => { :tag => 'legend',
292 :child => { :tag => 'legend',
293 :content => /Notes/ } }
293 :content => /Notes/ } }
294 end
294 end
295
295
296 def test_show_should_deny_anonymous_access_without_permission
296 def test_show_should_deny_anonymous_access_without_permission
297 Role.anonymous.remove_permission!(:view_issues)
297 Role.anonymous.remove_permission!(:view_issues)
298 get :show, :id => 1
298 get :show, :id => 1
299 assert_response :redirect
299 assert_response :redirect
300 end
300 end
301
301
302 def test_show_should_deny_non_member_access_without_permission
302 def test_show_should_deny_non_member_access_without_permission
303 Role.non_member.remove_permission!(:view_issues)
303 Role.non_member.remove_permission!(:view_issues)
304 @request.session[:user_id] = 9
304 @request.session[:user_id] = 9
305 get :show, :id => 1
305 get :show, :id => 1
306 assert_response 403
306 assert_response 403
307 end
307 end
308
308
309 def test_show_should_deny_member_access_without_permission
309 def test_show_should_deny_member_access_without_permission
310 Role.find(1).remove_permission!(:view_issues)
310 Role.find(1).remove_permission!(:view_issues)
311 @request.session[:user_id] = 2
311 @request.session[:user_id] = 2
312 get :show, :id => 1
312 get :show, :id => 1
313 assert_response 403
313 assert_response 403
314 end
314 end
315
315
316 def test_show_should_not_disclose_relations_to_invisible_issues
316 def test_show_should_not_disclose_relations_to_invisible_issues
317 Setting.cross_project_issue_relations = '1'
317 Setting.cross_project_issue_relations = '1'
318 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(2), :relation_type => 'relates')
318 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(2), :relation_type => 'relates')
319 # Relation to a private project issue
319 # Relation to a private project issue
320 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(4), :relation_type => 'relates')
320 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(4), :relation_type => 'relates')
321
321
322 get :show, :id => 1
322 get :show, :id => 1
323 assert_response :success
323 assert_response :success
324
324
325 assert_tag :div, :attributes => { :id => 'relations' },
325 assert_tag :div, :attributes => { :id => 'relations' },
326 :descendant => { :tag => 'a', :content => /#2$/ }
326 :descendant => { :tag => 'a', :content => /#2$/ }
327 assert_no_tag :div, :attributes => { :id => 'relations' },
327 assert_no_tag :div, :attributes => { :id => 'relations' },
328 :descendant => { :tag => 'a', :content => /#4$/ }
328 :descendant => { :tag => 'a', :content => /#4$/ }
329 end
329 end
330
330
331 def test_show_atom
331 def test_show_atom
332 get :show, :id => 2, :format => 'atom'
332 get :show, :id => 2, :format => 'atom'
333 assert_response :success
333 assert_response :success
334 assert_template 'journals/index.rxml'
334 assert_template 'journals/index.rxml'
335 # Inline image
335 # Inline image
336 assert_select 'content', :text => Regexp.new(Regexp.quote('http://test.host/attachments/download/10'))
336 assert_select 'content', :text => Regexp.new(Regexp.quote('http://test.host/attachments/download/10'))
337 end
337 end
338
338
339 def test_show_export_to_pdf
339 def test_show_export_to_pdf
340 get :show, :id => 3, :format => 'pdf'
340 get :show, :id => 3, :format => 'pdf'
341 assert_response :success
341 assert_response :success
342 assert_equal 'application/pdf', @response.content_type
342 assert_equal 'application/pdf', @response.content_type
343 assert @response.body.starts_with?('%PDF')
343 assert @response.body.starts_with?('%PDF')
344 assert_not_nil assigns(:issue)
344 assert_not_nil assigns(:issue)
345 end
345 end
346
346
347 def test_get_new
347 def test_get_new
348 @request.session[:user_id] = 2
348 @request.session[:user_id] = 2
349 get :new, :project_id => 1, :tracker_id => 1
349 get :new, :project_id => 1, :tracker_id => 1
350 assert_response :success
350 assert_response :success
351 assert_template 'new'
351 assert_template 'new'
352
352
353 assert_tag :tag => 'input', :attributes => { :name => 'issue[custom_field_values][2]',
353 assert_tag :tag => 'input', :attributes => { :name => 'issue[custom_field_values][2]',
354 :value => 'Default string' }
354 :value => 'Default string' }
355 end
355 end
356
356
357 def test_get_new_without_tracker_id
357 def test_get_new_without_tracker_id
358 @request.session[:user_id] = 2
358 @request.session[:user_id] = 2
359 get :new, :project_id => 1
359 get :new, :project_id => 1
360 assert_response :success
360 assert_response :success
361 assert_template 'new'
361 assert_template 'new'
362
362
363 issue = assigns(:issue)
363 issue = assigns(:issue)
364 assert_not_nil issue
364 assert_not_nil issue
365 assert_equal Project.find(1).trackers.first, issue.tracker
365 assert_equal Project.find(1).trackers.first, issue.tracker
366 end
366 end
367
367
368 def test_get_new_with_no_default_status_should_display_an_error
368 def test_get_new_with_no_default_status_should_display_an_error
369 @request.session[:user_id] = 2
369 @request.session[:user_id] = 2
370 IssueStatus.delete_all
370 IssueStatus.delete_all
371
371
372 get :new, :project_id => 1
372 get :new, :project_id => 1
373 assert_response 500
373 assert_response 500
374 assert_error_tag :content => /No default issue/
374 assert_error_tag :content => /No default issue/
375 end
375 end
376
376
377 def test_get_new_with_no_tracker_should_display_an_error
377 def test_get_new_with_no_tracker_should_display_an_error
378 @request.session[:user_id] = 2
378 @request.session[:user_id] = 2
379 Tracker.delete_all
379 Tracker.delete_all
380
380
381 get :new, :project_id => 1
381 get :new, :project_id => 1
382 assert_response 500
382 assert_response 500
383 assert_error_tag :content => /No tracker/
383 assert_error_tag :content => /No tracker/
384 end
384 end
385
385
386 def test_update_new_form
386 def test_update_new_form
387 @request.session[:user_id] = 2
387 @request.session[:user_id] = 2
388 xhr :post, :new, :project_id => 1,
388 xhr :post, :new, :project_id => 1,
389 :issue => {:tracker_id => 2,
389 :issue => {:tracker_id => 2,
390 :subject => 'This is the test_new issue',
390 :subject => 'This is the test_new issue',
391 :description => 'This is the description',
391 :description => 'This is the description',
392 :priority_id => 5}
392 :priority_id => 5}
393 assert_response :success
393 assert_response :success
394 assert_template 'attributes'
394 assert_template 'attributes'
395
395
396 issue = assigns(:issue)
396 issue = assigns(:issue)
397 assert_kind_of Issue, issue
397 assert_kind_of Issue, issue
398 assert_equal 1, issue.project_id
398 assert_equal 1, issue.project_id
399 assert_equal 2, issue.tracker_id
399 assert_equal 2, issue.tracker_id
400 assert_equal 'This is the test_new issue', issue.subject
400 assert_equal 'This is the test_new issue', issue.subject
401 end
401 end
402
402
403 def test_post_create
403 def test_post_create
404 @request.session[:user_id] = 2
404 @request.session[:user_id] = 2
405 assert_difference 'Issue.count' do
405 assert_difference 'Issue.count' do
406 post :create, :project_id => 1,
406 post :create, :project_id => 1,
407 :issue => {:tracker_id => 3,
407 :issue => {:tracker_id => 3,
408 :status_id => 2,
408 :status_id => 2,
409 :subject => 'This is the test_new issue',
409 :subject => 'This is the test_new issue',
410 :description => 'This is the description',
410 :description => 'This is the description',
411 :priority_id => 5,
411 :priority_id => 5,
412 :start_date => '2010-11-07',
412 :start_date => '2010-11-07',
413 :estimated_hours => '',
413 :estimated_hours => '',
414 :custom_field_values => {'2' => 'Value for field 2'}}
414 :custom_field_values => {'2' => 'Value for field 2'}}
415 end
415 end
416 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
416 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
417
417
418 issue = Issue.find_by_subject('This is the test_new issue')
418 issue = Issue.find_by_subject('This is the test_new issue')
419 assert_not_nil issue
419 assert_not_nil issue
420 assert_equal 2, issue.author_id
420 assert_equal 2, issue.author_id
421 assert_equal 3, issue.tracker_id
421 assert_equal 3, issue.tracker_id
422 assert_equal 2, issue.status_id
422 assert_equal 2, issue.status_id
423 assert_equal Date.parse('2010-11-07'), issue.start_date
423 assert_equal Date.parse('2010-11-07'), issue.start_date
424 assert_nil issue.estimated_hours
424 assert_nil issue.estimated_hours
425 v = issue.custom_values.find(:first, :conditions => {:custom_field_id => 2})
425 v = issue.custom_values.find(:first, :conditions => {:custom_field_id => 2})
426 assert_not_nil v
426 assert_not_nil v
427 assert_equal 'Value for field 2', v.value
427 assert_equal 'Value for field 2', v.value
428 end
428 end
429
429
430 def test_post_create_without_start_date
430 def test_post_create_without_start_date
431 @request.session[:user_id] = 2
431 @request.session[:user_id] = 2
432 assert_difference 'Issue.count' do
432 assert_difference 'Issue.count' do
433 post :create, :project_id => 1,
433 post :create, :project_id => 1,
434 :issue => {:tracker_id => 3,
434 :issue => {:tracker_id => 3,
435 :status_id => 2,
435 :status_id => 2,
436 :subject => 'This is the test_new issue',
436 :subject => 'This is the test_new issue',
437 :description => 'This is the description',
437 :description => 'This is the description',
438 :priority_id => 5,
438 :priority_id => 5,
439 :start_date => '',
439 :start_date => '',
440 :estimated_hours => '',
440 :estimated_hours => '',
441 :custom_field_values => {'2' => 'Value for field 2'}}
441 :custom_field_values => {'2' => 'Value for field 2'}}
442 end
442 end
443 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
443 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
444
444
445 issue = Issue.find_by_subject('This is the test_new issue')
445 issue = Issue.find_by_subject('This is the test_new issue')
446 assert_not_nil issue
446 assert_not_nil issue
447 assert_nil issue.start_date
447 assert_nil issue.start_date
448 end
448 end
449
449
450 def test_post_create_and_continue
450 def test_post_create_and_continue
451 @request.session[:user_id] = 2
451 @request.session[:user_id] = 2
452 post :create, :project_id => 1,
452 post :create, :project_id => 1,
453 :issue => {:tracker_id => 3,
453 :issue => {:tracker_id => 3,
454 :subject => 'This is first issue',
454 :subject => 'This is first issue',
455 :priority_id => 5},
455 :priority_id => 5},
456 :continue => ''
456 :continue => ''
457 assert_redirected_to :controller => 'issues', :action => 'new', :project_id => 'ecookbook',
457 assert_redirected_to :controller => 'issues', :action => 'new', :project_id => 'ecookbook',
458 :issue => {:tracker_id => 3}
458 :issue => {:tracker_id => 3}
459 end
459 end
460
460
461 def test_post_create_without_custom_fields_param
461 def test_post_create_without_custom_fields_param
462 @request.session[:user_id] = 2
462 @request.session[:user_id] = 2
463 assert_difference 'Issue.count' do
463 assert_difference 'Issue.count' do
464 post :create, :project_id => 1,
464 post :create, :project_id => 1,
465 :issue => {:tracker_id => 1,
465 :issue => {:tracker_id => 1,
466 :subject => 'This is the test_new issue',
466 :subject => 'This is the test_new issue',
467 :description => 'This is the description',
467 :description => 'This is the description',
468 :priority_id => 5}
468 :priority_id => 5}
469 end
469 end
470 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
470 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
471 end
471 end
472
472
473 def test_post_create_with_required_custom_field_and_without_custom_fields_param
473 def test_post_create_with_required_custom_field_and_without_custom_fields_param
474 field = IssueCustomField.find_by_name('Database')
474 field = IssueCustomField.find_by_name('Database')
475 field.update_attribute(:is_required, true)
475 field.update_attribute(:is_required, true)
476
476
477 @request.session[:user_id] = 2
477 @request.session[:user_id] = 2
478 post :create, :project_id => 1,
478 post :create, :project_id => 1,
479 :issue => {:tracker_id => 1,
479 :issue => {:tracker_id => 1,
480 :subject => 'This is the test_new issue',
480 :subject => 'This is the test_new issue',
481 :description => 'This is the description',
481 :description => 'This is the description',
482 :priority_id => 5}
482 :priority_id => 5}
483 assert_response :success
483 assert_response :success
484 assert_template 'new'
484 assert_template 'new'
485 issue = assigns(:issue)
485 issue = assigns(:issue)
486 assert_not_nil issue
486 assert_not_nil issue
487 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
487 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
488 end
488 end
489
489
490 def test_post_create_with_watchers
490 def test_post_create_with_watchers
491 @request.session[:user_id] = 2
491 @request.session[:user_id] = 2
492 ActionMailer::Base.deliveries.clear
492 ActionMailer::Base.deliveries.clear
493
493
494 assert_difference 'Watcher.count', 2 do
494 assert_difference 'Watcher.count', 2 do
495 post :create, :project_id => 1,
495 post :create, :project_id => 1,
496 :issue => {:tracker_id => 1,
496 :issue => {:tracker_id => 1,
497 :subject => 'This is a new issue with watchers',
497 :subject => 'This is a new issue with watchers',
498 :description => 'This is the description',
498 :description => 'This is the description',
499 :priority_id => 5,
499 :priority_id => 5,
500 :watcher_user_ids => ['2', '3']}
500 :watcher_user_ids => ['2', '3']}
501 end
501 end
502 issue = Issue.find_by_subject('This is a new issue with watchers')
502 issue = Issue.find_by_subject('This is a new issue with watchers')
503 assert_not_nil issue
503 assert_not_nil issue
504 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
504 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
505
505
506 # Watchers added
506 # Watchers added
507 assert_equal [2, 3], issue.watcher_user_ids.sort
507 assert_equal [2, 3], issue.watcher_user_ids.sort
508 assert issue.watched_by?(User.find(3))
508 assert issue.watched_by?(User.find(3))
509 # Watchers notified
509 # Watchers notified
510 mail = ActionMailer::Base.deliveries.last
510 mail = ActionMailer::Base.deliveries.last
511 assert_kind_of TMail::Mail, mail
511 assert_kind_of TMail::Mail, mail
512 assert [mail.bcc, mail.cc].flatten.include?(User.find(3).mail)
512 assert [mail.bcc, mail.cc].flatten.include?(User.find(3).mail)
513 end
513 end
514
514
515 def test_post_create_subissue
515 def test_post_create_subissue
516 @request.session[:user_id] = 2
516 @request.session[:user_id] = 2
517
517
518 assert_difference 'Issue.count' do
518 assert_difference 'Issue.count' do
519 post :create, :project_id => 1,
519 post :create, :project_id => 1,
520 :issue => {:tracker_id => 1,
520 :issue => {:tracker_id => 1,
521 :subject => 'This is a child issue',
521 :subject => 'This is a child issue',
522 :parent_issue_id => 2}
522 :parent_issue_id => 2}
523 end
523 end
524 issue = Issue.find_by_subject('This is a child issue')
524 issue = Issue.find_by_subject('This is a child issue')
525 assert_not_nil issue
525 assert_not_nil issue
526 assert_equal Issue.find(2), issue.parent
526 assert_equal Issue.find(2), issue.parent
527 end
527 end
528
528
529 def test_post_create_should_send_a_notification
529 def test_post_create_should_send_a_notification
530 ActionMailer::Base.deliveries.clear
530 ActionMailer::Base.deliveries.clear
531 @request.session[:user_id] = 2
531 @request.session[:user_id] = 2
532 assert_difference 'Issue.count' do
532 assert_difference 'Issue.count' do
533 post :create, :project_id => 1,
533 post :create, :project_id => 1,
534 :issue => {:tracker_id => 3,
534 :issue => {:tracker_id => 3,
535 :subject => 'This is the test_new issue',
535 :subject => 'This is the test_new issue',
536 :description => 'This is the description',
536 :description => 'This is the description',
537 :priority_id => 5,
537 :priority_id => 5,
538 :estimated_hours => '',
538 :estimated_hours => '',
539 :custom_field_values => {'2' => 'Value for field 2'}}
539 :custom_field_values => {'2' => 'Value for field 2'}}
540 end
540 end
541 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
541 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
542
542
543 assert_equal 1, ActionMailer::Base.deliveries.size
543 assert_equal 1, ActionMailer::Base.deliveries.size
544 end
544 end
545
545
546 def test_post_create_should_preserve_fields_values_on_validation_failure
546 def test_post_create_should_preserve_fields_values_on_validation_failure
547 @request.session[:user_id] = 2
547 @request.session[:user_id] = 2
548 post :create, :project_id => 1,
548 post :create, :project_id => 1,
549 :issue => {:tracker_id => 1,
549 :issue => {:tracker_id => 1,
550 # empty subject
550 # empty subject
551 :subject => '',
551 :subject => '',
552 :description => 'This is a description',
552 :description => 'This is a description',
553 :priority_id => 6,
553 :priority_id => 6,
554 :custom_field_values => {'1' => 'Oracle', '2' => 'Value for field 2'}}
554 :custom_field_values => {'1' => 'Oracle', '2' => 'Value for field 2'}}
555 assert_response :success
555 assert_response :success
556 assert_template 'new'
556 assert_template 'new'
557
557
558 assert_tag :textarea, :attributes => { :name => 'issue[description]' },
558 assert_tag :textarea, :attributes => { :name => 'issue[description]' },
559 :content => 'This is a description'
559 :content => 'This is a description'
560 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
560 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
561 :child => { :tag => 'option', :attributes => { :selected => 'selected',
561 :child => { :tag => 'option', :attributes => { :selected => 'selected',
562 :value => '6' },
562 :value => '6' },
563 :content => 'High' }
563 :content => 'High' }
564 # Custom fields
564 # Custom fields
565 assert_tag :select, :attributes => { :name => 'issue[custom_field_values][1]' },
565 assert_tag :select, :attributes => { :name => 'issue[custom_field_values][1]' },
566 :child => { :tag => 'option', :attributes => { :selected => 'selected',
566 :child => { :tag => 'option', :attributes => { :selected => 'selected',
567 :value => 'Oracle' },
567 :value => 'Oracle' },
568 :content => 'Oracle' }
568 :content => 'Oracle' }
569 assert_tag :input, :attributes => { :name => 'issue[custom_field_values][2]',
569 assert_tag :input, :attributes => { :name => 'issue[custom_field_values][2]',
570 :value => 'Value for field 2'}
570 :value => 'Value for field 2'}
571 end
571 end
572
572
573 def test_post_create_should_ignore_non_safe_attributes
573 def test_post_create_should_ignore_non_safe_attributes
574 @request.session[:user_id] = 2
574 @request.session[:user_id] = 2
575 assert_nothing_raised do
575 assert_nothing_raised do
576 post :create, :project_id => 1, :issue => { :tracker => "A param can not be a Tracker" }
576 post :create, :project_id => 1, :issue => { :tracker => "A param can not be a Tracker" }
577 end
577 end
578 end
578 end
579
579
580 context "without workflow privilege" do
580 context "without workflow privilege" do
581 setup do
581 setup do
582 Workflow.delete_all(["role_id = ?", Role.anonymous.id])
582 Workflow.delete_all(["role_id = ?", Role.anonymous.id])
583 Role.anonymous.add_permission! :add_issues
583 Role.anonymous.add_permission! :add_issues, :add_issue_notes
584 end
584 end
585
585
586 context "#new" do
586 context "#new" do
587 should "propose default status only" do
587 should "propose default status only" do
588 get :new, :project_id => 1
588 get :new, :project_id => 1
589 assert_response :success
589 assert_response :success
590 assert_template 'new'
590 assert_template 'new'
591 assert_tag :tag => 'select',
591 assert_tag :tag => 'select',
592 :attributes => {:name => 'issue[status_id]'},
592 :attributes => {:name => 'issue[status_id]'},
593 :children => {:count => 1},
593 :children => {:count => 1},
594 :child => {:tag => 'option', :attributes => {:value => IssueStatus.default.id.to_s}}
594 :child => {:tag => 'option', :attributes => {:value => IssueStatus.default.id.to_s}}
595 end
595 end
596
596
597 should "accept default status" do
597 should "accept default status" do
598 assert_difference 'Issue.count' do
598 assert_difference 'Issue.count' do
599 post :create, :project_id => 1,
599 post :create, :project_id => 1,
600 :issue => {:tracker_id => 1,
600 :issue => {:tracker_id => 1,
601 :subject => 'This is an issue',
601 :subject => 'This is an issue',
602 :status_id => 1}
602 :status_id => 1}
603 end
603 end
604 issue = Issue.last(:order => 'id')
604 issue = Issue.last(:order => 'id')
605 assert_equal IssueStatus.default, issue.status
605 assert_equal IssueStatus.default, issue.status
606 end
606 end
607
607
608 should "accept default status" do
609 assert_difference 'Issue.count' do
610 post :create, :project_id => 1,
611 :issue => {:tracker_id => 1,
612 :subject => 'This is an issue',
613 :status_id => 1}
614 end
615 issue = Issue.last(:order => 'id')
616 assert_equal IssueStatus.default, issue.status
617 end
618
608 should "ignore unauthorized status" do
619 should "ignore unauthorized status" do
609 assert_difference 'Issue.count' do
620 assert_difference 'Issue.count' do
610 post :create, :project_id => 1,
621 post :create, :project_id => 1,
611 :issue => {:tracker_id => 1,
622 :issue => {:tracker_id => 1,
612 :subject => 'This is an issue',
623 :subject => 'This is an issue',
613 :status_id => 3}
624 :status_id => 3}
614 end
625 end
615 issue = Issue.last(:order => 'id')
626 issue = Issue.last(:order => 'id')
616 assert_equal IssueStatus.default, issue.status
627 assert_equal IssueStatus.default, issue.status
617 end
628 end
618 end
629 end
630
631 context "#update" do
632 should "ignore status change" do
633 assert_difference 'Journal.count' do
634 put :update, :id => 1, :notes => 'just trying', :issue => {:status_id => 3}
635 end
636 assert_equal 1, Issue.find(1).status_id
637 end
638
639 should "ignore attributes changes" do
640 assert_difference 'Journal.count' do
641 put :update, :id => 1, :notes => 'just trying', :issue => {:subject => 'changed', :assigned_to_id => 2}
642 end
643 issue = Issue.find(1)
644 assert_equal "Can't print recipes", issue.subject
645 assert_nil issue.assigned_to
646 end
647 end
648 end
649
650 context "with workflow privilege" do
651 setup do
652 Workflow.delete_all(["role_id = ?", Role.anonymous.id])
653 Workflow.create!(:role => Role.anonymous, :tracker_id => 1, :old_status_id => 1, :new_status_id => 3)
654 Workflow.create!(:role => Role.anonymous, :tracker_id => 1, :old_status_id => 1, :new_status_id => 4)
655 Role.anonymous.add_permission! :add_issues, :add_issue_notes
656 end
657
658 context "#update" do
659 should "accept authorized status" do
660 assert_difference 'Journal.count' do
661 put :update, :id => 1, :notes => 'just trying', :issue => {:status_id => 3}
662 end
663 assert_equal 3, Issue.find(1).status_id
664 end
665
666 should "ignore unauthorized status" do
667 assert_difference 'Journal.count' do
668 put :update, :id => 1, :notes => 'just trying', :issue => {:status_id => 2}
669 end
670 assert_equal 1, Issue.find(1).status_id
671 end
672
673 should "accept authorized attributes changes" do
674 assert_difference 'Journal.count' do
675 put :update, :id => 1, :notes => 'just trying', :issue => {:assigned_to_id => 2}
676 end
677 issue = Issue.find(1)
678 assert_equal 2, issue.assigned_to_id
679 end
680
681 should "ignore unauthorized attributes changes" do
682 assert_difference 'Journal.count' do
683 put :update, :id => 1, :notes => 'just trying', :issue => {:subject => 'changed'}
684 end
685 issue = Issue.find(1)
686 assert_equal "Can't print recipes", issue.subject
687 end
688 end
689
690 context "and :edit_issues permission" do
691 setup do
692 Role.anonymous.add_permission! :add_issues, :edit_issues
693 end
694
695 should "accept authorized status" do
696 assert_difference 'Journal.count' do
697 put :update, :id => 1, :notes => 'just trying', :issue => {:status_id => 3}
698 end
699 assert_equal 3, Issue.find(1).status_id
700 end
701
702 should "ignore unauthorized status" do
703 assert_difference 'Journal.count' do
704 put :update, :id => 1, :notes => 'just trying', :issue => {:status_id => 2}
705 end
706 assert_equal 1, Issue.find(1).status_id
707 end
708
709 should "accept authorized attributes changes" do
710 assert_difference 'Journal.count' do
711 put :update, :id => 1, :notes => 'just trying', :issue => {:subject => 'changed', :assigned_to_id => 2}
712 end
713 issue = Issue.find(1)
714 assert_equal "changed", issue.subject
715 assert_equal 2, issue.assigned_to_id
716 end
717 end
619 end
718 end
620
719
621 def test_copy_issue
720 def test_copy_issue
622 @request.session[:user_id] = 2
721 @request.session[:user_id] = 2
623 get :new, :project_id => 1, :copy_from => 1
722 get :new, :project_id => 1, :copy_from => 1
624 assert_template 'new'
723 assert_template 'new'
625 assert_not_nil assigns(:issue)
724 assert_not_nil assigns(:issue)
626 orig = Issue.find(1)
725 orig = Issue.find(1)
627 assert_equal orig.subject, assigns(:issue).subject
726 assert_equal orig.subject, assigns(:issue).subject
628 end
727 end
629
728
630 def test_get_edit
729 def test_get_edit
631 @request.session[:user_id] = 2
730 @request.session[:user_id] = 2
632 get :edit, :id => 1
731 get :edit, :id => 1
633 assert_response :success
732 assert_response :success
634 assert_template 'edit'
733 assert_template 'edit'
635 assert_not_nil assigns(:issue)
734 assert_not_nil assigns(:issue)
636 assert_equal Issue.find(1), assigns(:issue)
735 assert_equal Issue.find(1), assigns(:issue)
637 end
736 end
638
737
639 def test_get_edit_with_params
738 def test_get_edit_with_params
640 @request.session[:user_id] = 2
739 @request.session[:user_id] = 2
641 get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 }
740 get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 }
642 assert_response :success
741 assert_response :success
643 assert_template 'edit'
742 assert_template 'edit'
644
743
645 issue = assigns(:issue)
744 issue = assigns(:issue)
646 assert_not_nil issue
745 assert_not_nil issue
647
746
648 assert_equal 5, issue.status_id
747 assert_equal 5, issue.status_id
649 assert_tag :select, :attributes => { :name => 'issue[status_id]' },
748 assert_tag :select, :attributes => { :name => 'issue[status_id]' },
650 :child => { :tag => 'option',
749 :child => { :tag => 'option',
651 :content => 'Closed',
750 :content => 'Closed',
652 :attributes => { :selected => 'selected' } }
751 :attributes => { :selected => 'selected' } }
653
752
654 assert_equal 7, issue.priority_id
753 assert_equal 7, issue.priority_id
655 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
754 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
656 :child => { :tag => 'option',
755 :child => { :tag => 'option',
657 :content => 'Urgent',
756 :content => 'Urgent',
658 :attributes => { :selected => 'selected' } }
757 :attributes => { :selected => 'selected' } }
659 end
758 end
660
759
661 def test_update_edit_form
760 def test_update_edit_form
662 @request.session[:user_id] = 2
761 @request.session[:user_id] = 2
663 xhr :post, :new, :project_id => 1,
762 xhr :post, :new, :project_id => 1,
664 :id => 1,
763 :id => 1,
665 :issue => {:tracker_id => 2,
764 :issue => {:tracker_id => 2,
666 :subject => 'This is the test_new issue',
765 :subject => 'This is the test_new issue',
667 :description => 'This is the description',
766 :description => 'This is the description',
668 :priority_id => 5}
767 :priority_id => 5}
669 assert_response :success
768 assert_response :success
670 assert_template 'attributes'
769 assert_template 'attributes'
671
770
672 issue = assigns(:issue)
771 issue = assigns(:issue)
673 assert_kind_of Issue, issue
772 assert_kind_of Issue, issue
674 assert_equal 1, issue.id
773 assert_equal 1, issue.id
675 assert_equal 1, issue.project_id
774 assert_equal 1, issue.project_id
676 assert_equal 2, issue.tracker_id
775 assert_equal 2, issue.tracker_id
677 assert_equal 'This is the test_new issue', issue.subject
776 assert_equal 'This is the test_new issue', issue.subject
678 end
777 end
679
778
680 def test_update_using_invalid_http_verbs
779 def test_update_using_invalid_http_verbs
681 @request.session[:user_id] = 2
780 @request.session[:user_id] = 2
682 subject = 'Updated by an invalid http verb'
781 subject = 'Updated by an invalid http verb'
683
782
684 get :update, :id => 1, :issue => {:subject => subject}
783 get :update, :id => 1, :issue => {:subject => subject}
685 assert_not_equal subject, Issue.find(1).subject
784 assert_not_equal subject, Issue.find(1).subject
686
785
687 post :update, :id => 1, :issue => {:subject => subject}
786 post :update, :id => 1, :issue => {:subject => subject}
688 assert_not_equal subject, Issue.find(1).subject
787 assert_not_equal subject, Issue.find(1).subject
689
788
690 delete :update, :id => 1, :issue => {:subject => subject}
789 delete :update, :id => 1, :issue => {:subject => subject}
691 assert_not_equal subject, Issue.find(1).subject
790 assert_not_equal subject, Issue.find(1).subject
692 end
791 end
693
792
694 def test_put_update_without_custom_fields_param
793 def test_put_update_without_custom_fields_param
695 @request.session[:user_id] = 2
794 @request.session[:user_id] = 2
696 ActionMailer::Base.deliveries.clear
795 ActionMailer::Base.deliveries.clear
697
796
698 issue = Issue.find(1)
797 issue = Issue.find(1)
699 assert_equal '125', issue.custom_value_for(2).value
798 assert_equal '125', issue.custom_value_for(2).value
700 old_subject = issue.subject
799 old_subject = issue.subject
701 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
800 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
702
801
703 assert_difference('Journal.count') do
802 assert_difference('Journal.count') do
704 assert_difference('JournalDetail.count', 2) do
803 assert_difference('JournalDetail.count', 2) do
705 put :update, :id => 1, :issue => {:subject => new_subject,
804 put :update, :id => 1, :issue => {:subject => new_subject,
706 :priority_id => '6',
805 :priority_id => '6',
707 :category_id => '1' # no change
806 :category_id => '1' # no change
708 }
807 }
709 end
808 end
710 end
809 end
711 assert_redirected_to :action => 'show', :id => '1'
810 assert_redirected_to :action => 'show', :id => '1'
712 issue.reload
811 issue.reload
713 assert_equal new_subject, issue.subject
812 assert_equal new_subject, issue.subject
714 # Make sure custom fields were not cleared
813 # Make sure custom fields were not cleared
715 assert_equal '125', issue.custom_value_for(2).value
814 assert_equal '125', issue.custom_value_for(2).value
716
815
717 mail = ActionMailer::Base.deliveries.last
816 mail = ActionMailer::Base.deliveries.last
718 assert_kind_of TMail::Mail, mail
817 assert_kind_of TMail::Mail, mail
719 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
818 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
720 assert mail.body.include?("Subject changed from #{old_subject} to #{new_subject}")
819 assert mail.body.include?("Subject changed from #{old_subject} to #{new_subject}")
721 end
820 end
722
821
723 def test_put_update_with_custom_field_change
822 def test_put_update_with_custom_field_change
724 @request.session[:user_id] = 2
823 @request.session[:user_id] = 2
725 issue = Issue.find(1)
824 issue = Issue.find(1)
726 assert_equal '125', issue.custom_value_for(2).value
825 assert_equal '125', issue.custom_value_for(2).value
727
826
728 assert_difference('Journal.count') do
827 assert_difference('Journal.count') do
729 assert_difference('JournalDetail.count', 3) do
828 assert_difference('JournalDetail.count', 3) do
730 put :update, :id => 1, :issue => {:subject => 'Custom field change',
829 put :update, :id => 1, :issue => {:subject => 'Custom field change',
731 :priority_id => '6',
830 :priority_id => '6',
732 :category_id => '1', # no change
831 :category_id => '1', # no change
733 :custom_field_values => { '2' => 'New custom value' }
832 :custom_field_values => { '2' => 'New custom value' }
734 }
833 }
735 end
834 end
736 end
835 end
737 assert_redirected_to :action => 'show', :id => '1'
836 assert_redirected_to :action => 'show', :id => '1'
738 issue.reload
837 issue.reload
739 assert_equal 'New custom value', issue.custom_value_for(2).value
838 assert_equal 'New custom value', issue.custom_value_for(2).value
740
839
741 mail = ActionMailer::Base.deliveries.last
840 mail = ActionMailer::Base.deliveries.last
742 assert_kind_of TMail::Mail, mail
841 assert_kind_of TMail::Mail, mail
743 assert mail.body.include?("Searchable field changed from 125 to New custom value")
842 assert mail.body.include?("Searchable field changed from 125 to New custom value")
744 end
843 end
745
844
746 def test_put_update_with_status_and_assignee_change
845 def test_put_update_with_status_and_assignee_change
747 issue = Issue.find(1)
846 issue = Issue.find(1)
748 assert_equal 1, issue.status_id
847 assert_equal 1, issue.status_id
749 @request.session[:user_id] = 2
848 @request.session[:user_id] = 2
750 assert_difference('TimeEntry.count', 0) do
849 assert_difference('TimeEntry.count', 0) do
751 put :update,
850 put :update,
752 :id => 1,
851 :id => 1,
753 :issue => { :status_id => 2, :assigned_to_id => 3 },
852 :issue => { :status_id => 2, :assigned_to_id => 3 },
754 :notes => 'Assigned to dlopper',
853 :notes => 'Assigned to dlopper',
755 :time_entry => { :hours => '', :comments => '', :activity_id => TimeEntryActivity.first }
854 :time_entry => { :hours => '', :comments => '', :activity_id => TimeEntryActivity.first }
756 end
855 end
757 assert_redirected_to :action => 'show', :id => '1'
856 assert_redirected_to :action => 'show', :id => '1'
758 issue.reload
857 issue.reload
759 assert_equal 2, issue.status_id
858 assert_equal 2, issue.status_id
760 j = Journal.find(:first, :order => 'id DESC')
859 j = Journal.find(:first, :order => 'id DESC')
761 assert_equal 'Assigned to dlopper', j.notes
860 assert_equal 'Assigned to dlopper', j.notes
762 assert_equal 2, j.details.size
861 assert_equal 2, j.details.size
763
862
764 mail = ActionMailer::Base.deliveries.last
863 mail = ActionMailer::Base.deliveries.last
765 assert mail.body.include?("Status changed from New to Assigned")
864 assert mail.body.include?("Status changed from New to Assigned")
766 # subject should contain the new status
865 # subject should contain the new status
767 assert mail.subject.include?("(#{ IssueStatus.find(2).name })")
866 assert mail.subject.include?("(#{ IssueStatus.find(2).name })")
768 end
867 end
769
868
770 def test_put_update_with_note_only
869 def test_put_update_with_note_only
771 notes = 'Note added by IssuesControllerTest#test_update_with_note_only'
870 notes = 'Note added by IssuesControllerTest#test_update_with_note_only'
772 # anonymous user
871 # anonymous user
773 put :update,
872 put :update,
774 :id => 1,
873 :id => 1,
775 :notes => notes
874 :notes => notes
776 assert_redirected_to :action => 'show', :id => '1'
875 assert_redirected_to :action => 'show', :id => '1'
777 j = Journal.find(:first, :order => 'id DESC')
876 j = Journal.find(:first, :order => 'id DESC')
778 assert_equal notes, j.notes
877 assert_equal notes, j.notes
779 assert_equal 0, j.details.size
878 assert_equal 0, j.details.size
780 assert_equal User.anonymous, j.user
879 assert_equal User.anonymous, j.user
781
880
782 mail = ActionMailer::Base.deliveries.last
881 mail = ActionMailer::Base.deliveries.last
783 assert mail.body.include?(notes)
882 assert mail.body.include?(notes)
784 end
883 end
785
884
786 def test_put_update_with_note_and_spent_time
885 def test_put_update_with_note_and_spent_time
787 @request.session[:user_id] = 2
886 @request.session[:user_id] = 2
788 spent_hours_before = Issue.find(1).spent_hours
887 spent_hours_before = Issue.find(1).spent_hours
789 assert_difference('TimeEntry.count') do
888 assert_difference('TimeEntry.count') do
790 put :update,
889 put :update,
791 :id => 1,
890 :id => 1,
792 :notes => '2.5 hours added',
891 :notes => '2.5 hours added',
793 :time_entry => { :hours => '2.5', :comments => 'test_put_update_with_note_and_spent_time', :activity_id => TimeEntryActivity.first.id }
892 :time_entry => { :hours => '2.5', :comments => 'test_put_update_with_note_and_spent_time', :activity_id => TimeEntryActivity.first.id }
794 end
893 end
795 assert_redirected_to :action => 'show', :id => '1'
894 assert_redirected_to :action => 'show', :id => '1'
796
895
797 issue = Issue.find(1)
896 issue = Issue.find(1)
798
897
799 j = Journal.find(:first, :order => 'id DESC')
898 j = Journal.find(:first, :order => 'id DESC')
800 assert_equal '2.5 hours added', j.notes
899 assert_equal '2.5 hours added', j.notes
801 assert_equal 0, j.details.size
900 assert_equal 0, j.details.size
802
901
803 t = issue.time_entries.find_by_comments('test_put_update_with_note_and_spent_time')
902 t = issue.time_entries.find_by_comments('test_put_update_with_note_and_spent_time')
804 assert_not_nil t
903 assert_not_nil t
805 assert_equal 2.5, t.hours
904 assert_equal 2.5, t.hours
806 assert_equal spent_hours_before + 2.5, issue.spent_hours
905 assert_equal spent_hours_before + 2.5, issue.spent_hours
807 end
906 end
808
907
809 def test_put_update_with_attachment_only
908 def test_put_update_with_attachment_only
810 set_tmp_attachments_directory
909 set_tmp_attachments_directory
811
910
812 # Delete all fixtured journals, a race condition can occur causing the wrong
911 # Delete all fixtured journals, a race condition can occur causing the wrong
813 # journal to get fetched in the next find.
912 # journal to get fetched in the next find.
814 Journal.delete_all
913 Journal.delete_all
815
914
816 # anonymous user
915 # anonymous user
817 put :update,
916 put :update,
818 :id => 1,
917 :id => 1,
819 :notes => '',
918 :notes => '',
820 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
919 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
821 assert_redirected_to :action => 'show', :id => '1'
920 assert_redirected_to :action => 'show', :id => '1'
822 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
921 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
823 assert j.notes.blank?
922 assert j.notes.blank?
824 assert_equal 1, j.details.size
923 assert_equal 1, j.details.size
825 assert_equal 'testfile.txt', j.details.first.value
924 assert_equal 'testfile.txt', j.details.first.value
826 assert_equal User.anonymous, j.user
925 assert_equal User.anonymous, j.user
827
926
828 mail = ActionMailer::Base.deliveries.last
927 mail = ActionMailer::Base.deliveries.last
829 assert mail.body.include?('testfile.txt')
928 assert mail.body.include?('testfile.txt')
830 end
929 end
831
930
832 def test_put_update_with_attachment_that_fails_to_save
931 def test_put_update_with_attachment_that_fails_to_save
833 set_tmp_attachments_directory
932 set_tmp_attachments_directory
834
933
835 # Delete all fixtured journals, a race condition can occur causing the wrong
934 # Delete all fixtured journals, a race condition can occur causing the wrong
836 # journal to get fetched in the next find.
935 # journal to get fetched in the next find.
837 Journal.delete_all
936 Journal.delete_all
838
937
839 # Mock out the unsaved attachment
938 # Mock out the unsaved attachment
840 Attachment.any_instance.stubs(:create).returns(Attachment.new)
939 Attachment.any_instance.stubs(:create).returns(Attachment.new)
841
940
842 # anonymous user
941 # anonymous user
843 put :update,
942 put :update,
844 :id => 1,
943 :id => 1,
845 :notes => '',
944 :notes => '',
846 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
945 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
847 assert_redirected_to :action => 'show', :id => '1'
946 assert_redirected_to :action => 'show', :id => '1'
848 assert_equal '1 file(s) could not be saved.', flash[:warning]
947 assert_equal '1 file(s) could not be saved.', flash[:warning]
849
948
850 end if Object.const_defined?(:Mocha)
949 end if Object.const_defined?(:Mocha)
851
950
852 def test_put_update_with_no_change
951 def test_put_update_with_no_change
853 issue = Issue.find(1)
952 issue = Issue.find(1)
854 issue.journals.clear
953 issue.journals.clear
855 ActionMailer::Base.deliveries.clear
954 ActionMailer::Base.deliveries.clear
856
955
857 put :update,
956 put :update,
858 :id => 1,
957 :id => 1,
859 :notes => ''
958 :notes => ''
860 assert_redirected_to :action => 'show', :id => '1'
959 assert_redirected_to :action => 'show', :id => '1'
861
960
862 issue.reload
961 issue.reload
863 assert issue.journals.empty?
962 assert issue.journals.empty?
864 # No email should be sent
963 # No email should be sent
865 assert ActionMailer::Base.deliveries.empty?
964 assert ActionMailer::Base.deliveries.empty?
866 end
965 end
867
966
868 def test_put_update_should_send_a_notification
967 def test_put_update_should_send_a_notification
869 @request.session[:user_id] = 2
968 @request.session[:user_id] = 2
870 ActionMailer::Base.deliveries.clear
969 ActionMailer::Base.deliveries.clear
871 issue = Issue.find(1)
970 issue = Issue.find(1)
872 old_subject = issue.subject
971 old_subject = issue.subject
873 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
972 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
874
973
875 put :update, :id => 1, :issue => {:subject => new_subject,
974 put :update, :id => 1, :issue => {:subject => new_subject,
876 :priority_id => '6',
975 :priority_id => '6',
877 :category_id => '1' # no change
976 :category_id => '1' # no change
878 }
977 }
879 assert_equal 1, ActionMailer::Base.deliveries.size
978 assert_equal 1, ActionMailer::Base.deliveries.size
880 end
979 end
881
980
882 def test_put_update_with_invalid_spent_time
981 def test_put_update_with_invalid_spent_time
883 @request.session[:user_id] = 2
982 @request.session[:user_id] = 2
884 notes = 'Note added by IssuesControllerTest#test_post_edit_with_invalid_spent_time'
983 notes = 'Note added by IssuesControllerTest#test_post_edit_with_invalid_spent_time'
885
984
886 assert_no_difference('Journal.count') do
985 assert_no_difference('Journal.count') do
887 put :update,
986 put :update,
888 :id => 1,
987 :id => 1,
889 :notes => notes,
988 :notes => notes,
890 :time_entry => {"comments"=>"", "activity_id"=>"", "hours"=>"2z"}
989 :time_entry => {"comments"=>"", "activity_id"=>"", "hours"=>"2z"}
891 end
990 end
892 assert_response :success
991 assert_response :success
893 assert_template 'edit'
992 assert_template 'edit'
894
993
895 assert_tag :textarea, :attributes => { :name => 'notes' },
994 assert_tag :textarea, :attributes => { :name => 'notes' },
896 :content => notes
995 :content => notes
897 assert_tag :input, :attributes => { :name => 'time_entry[hours]', :value => "2z" }
996 assert_tag :input, :attributes => { :name => 'time_entry[hours]', :value => "2z" }
898 end
997 end
899
998
900 def test_put_update_should_allow_fixed_version_to_be_set_to_a_subproject
999 def test_put_update_should_allow_fixed_version_to_be_set_to_a_subproject
901 issue = Issue.find(2)
1000 issue = Issue.find(2)
902 @request.session[:user_id] = 2
1001 @request.session[:user_id] = 2
903
1002
904 put :update,
1003 put :update,
905 :id => issue.id,
1004 :id => issue.id,
906 :issue => {
1005 :issue => {
907 :fixed_version_id => 4
1006 :fixed_version_id => 4
908 }
1007 }
909
1008
910 assert_response :redirect
1009 assert_response :redirect
911 issue.reload
1010 issue.reload
912 assert_equal 4, issue.fixed_version_id
1011 assert_equal 4, issue.fixed_version_id
913 assert_not_equal issue.project_id, issue.fixed_version.project_id
1012 assert_not_equal issue.project_id, issue.fixed_version.project_id
914 end
1013 end
915
1014
916 def test_put_update_should_redirect_back_using_the_back_url_parameter
1015 def test_put_update_should_redirect_back_using_the_back_url_parameter
917 issue = Issue.find(2)
1016 issue = Issue.find(2)
918 @request.session[:user_id] = 2
1017 @request.session[:user_id] = 2
919
1018
920 put :update,
1019 put :update,
921 :id => issue.id,
1020 :id => issue.id,
922 :issue => {
1021 :issue => {
923 :fixed_version_id => 4
1022 :fixed_version_id => 4
924 },
1023 },
925 :back_url => '/issues'
1024 :back_url => '/issues'
926
1025
927 assert_response :redirect
1026 assert_response :redirect
928 assert_redirected_to '/issues'
1027 assert_redirected_to '/issues'
929 end
1028 end
930
1029
931 def test_put_update_should_not_redirect_back_using_the_back_url_parameter_off_the_host
1030 def test_put_update_should_not_redirect_back_using_the_back_url_parameter_off_the_host
932 issue = Issue.find(2)
1031 issue = Issue.find(2)
933 @request.session[:user_id] = 2
1032 @request.session[:user_id] = 2
934
1033
935 put :update,
1034 put :update,
936 :id => issue.id,
1035 :id => issue.id,
937 :issue => {
1036 :issue => {
938 :fixed_version_id => 4
1037 :fixed_version_id => 4
939 },
1038 },
940 :back_url => 'http://google.com'
1039 :back_url => 'http://google.com'
941
1040
942 assert_response :redirect
1041 assert_response :redirect
943 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue.id
1042 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue.id
944 end
1043 end
945
1044
946 def test_get_bulk_edit
1045 def test_get_bulk_edit
947 @request.session[:user_id] = 2
1046 @request.session[:user_id] = 2
948 get :bulk_edit, :ids => [1, 2]
1047 get :bulk_edit, :ids => [1, 2]
949 assert_response :success
1048 assert_response :success
950 assert_template 'bulk_edit'
1049 assert_template 'bulk_edit'
951
1050
952 # Project specific custom field, date type
1051 # Project specific custom field, date type
953 field = CustomField.find(9)
1052 field = CustomField.find(9)
954 assert !field.is_for_all?
1053 assert !field.is_for_all?
955 assert_equal 'date', field.field_format
1054 assert_equal 'date', field.field_format
956 assert_tag :input, :attributes => {:name => 'issue[custom_field_values][9]'}
1055 assert_tag :input, :attributes => {:name => 'issue[custom_field_values][9]'}
957
1056
958 # System wide custom field
1057 # System wide custom field
959 assert CustomField.find(1).is_for_all?
1058 assert CustomField.find(1).is_for_all?
960 assert_tag :select, :attributes => {:name => 'issue[custom_field_values][1]'}
1059 assert_tag :select, :attributes => {:name => 'issue[custom_field_values][1]'}
961 end
1060 end
962
1061
963 def test_get_bulk_edit_on_different_projects
1062 def test_get_bulk_edit_on_different_projects
964 @request.session[:user_id] = 2
1063 @request.session[:user_id] = 2
965 get :bulk_edit, :ids => [1, 2, 6]
1064 get :bulk_edit, :ids => [1, 2, 6]
966 assert_response :success
1065 assert_response :success
967 assert_template 'bulk_edit'
1066 assert_template 'bulk_edit'
968
1067
969 # Project specific custom field, date type
1068 # Project specific custom field, date type
970 field = CustomField.find(9)
1069 field = CustomField.find(9)
971 assert !field.is_for_all?
1070 assert !field.is_for_all?
972 assert !field.project_ids.include?(Issue.find(6).project_id)
1071 assert !field.project_ids.include?(Issue.find(6).project_id)
973 assert_no_tag :input, :attributes => {:name => 'issue[custom_field_values][9]'}
1072 assert_no_tag :input, :attributes => {:name => 'issue[custom_field_values][9]'}
974 end
1073 end
975
1074
976 def test_bulk_update
1075 def test_bulk_update
977 @request.session[:user_id] = 2
1076 @request.session[:user_id] = 2
978 # update issues priority
1077 # update issues priority
979 post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing',
1078 post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing',
980 :issue => {:priority_id => 7,
1079 :issue => {:priority_id => 7,
981 :assigned_to_id => '',
1080 :assigned_to_id => '',
982 :custom_field_values => {'2' => ''}}
1081 :custom_field_values => {'2' => ''}}
983
1082
984 assert_response 302
1083 assert_response 302
985 # check that the issues were updated
1084 # check that the issues were updated
986 assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id}
1085 assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id}
987
1086
988 issue = Issue.find(1)
1087 issue = Issue.find(1)
989 journal = issue.journals.find(:first, :order => 'created_on DESC')
1088 journal = issue.journals.find(:first, :order => 'created_on DESC')
990 assert_equal '125', issue.custom_value_for(2).value
1089 assert_equal '125', issue.custom_value_for(2).value
991 assert_equal 'Bulk editing', journal.notes
1090 assert_equal 'Bulk editing', journal.notes
992 assert_equal 1, journal.details.size
1091 assert_equal 1, journal.details.size
993 end
1092 end
994
1093
995 def test_bulk_update_on_different_projects
1094 def test_bulk_update_on_different_projects
996 @request.session[:user_id] = 2
1095 @request.session[:user_id] = 2
997 # update issues priority
1096 # update issues priority
998 post :bulk_update, :ids => [1, 2, 6], :notes => 'Bulk editing',
1097 post :bulk_update, :ids => [1, 2, 6], :notes => 'Bulk editing',
999 :issue => {:priority_id => 7,
1098 :issue => {:priority_id => 7,
1000 :assigned_to_id => '',
1099 :assigned_to_id => '',
1001 :custom_field_values => {'2' => ''}}
1100 :custom_field_values => {'2' => ''}}
1002
1101
1003 assert_response 302
1102 assert_response 302
1004 # check that the issues were updated
1103 # check that the issues were updated
1005 assert_equal [7, 7, 7], Issue.find([1,2,6]).map(&:priority_id)
1104 assert_equal [7, 7, 7], Issue.find([1,2,6]).map(&:priority_id)
1006
1105
1007 issue = Issue.find(1)
1106 issue = Issue.find(1)
1008 journal = issue.journals.find(:first, :order => 'created_on DESC')
1107 journal = issue.journals.find(:first, :order => 'created_on DESC')
1009 assert_equal '125', issue.custom_value_for(2).value
1108 assert_equal '125', issue.custom_value_for(2).value
1010 assert_equal 'Bulk editing', journal.notes
1109 assert_equal 'Bulk editing', journal.notes
1011 assert_equal 1, journal.details.size
1110 assert_equal 1, journal.details.size
1012 end
1111 end
1013
1112
1014 def test_bulk_update_on_different_projects_without_rights
1113 def test_bulk_update_on_different_projects_without_rights
1015 @request.session[:user_id] = 3
1114 @request.session[:user_id] = 3
1016 user = User.find(3)
1115 user = User.find(3)
1017 action = { :controller => "issues", :action => "bulk_update" }
1116 action = { :controller => "issues", :action => "bulk_update" }
1018 assert user.allowed_to?(action, Issue.find(1).project)
1117 assert user.allowed_to?(action, Issue.find(1).project)
1019 assert ! user.allowed_to?(action, Issue.find(6).project)
1118 assert ! user.allowed_to?(action, Issue.find(6).project)
1020 post :bulk_update, :ids => [1, 6], :notes => 'Bulk should fail',
1119 post :bulk_update, :ids => [1, 6], :notes => 'Bulk should fail',
1021 :issue => {:priority_id => 7,
1120 :issue => {:priority_id => 7,
1022 :assigned_to_id => '',
1121 :assigned_to_id => '',
1023 :custom_field_values => {'2' => ''}}
1122 :custom_field_values => {'2' => ''}}
1024 assert_response 403
1123 assert_response 403
1025 assert_not_equal "Bulk should fail", Journal.last.notes
1124 assert_not_equal "Bulk should fail", Journal.last.notes
1026 end
1125 end
1027
1126
1028 def test_bullk_update_should_send_a_notification
1127 def test_bullk_update_should_send_a_notification
1029 @request.session[:user_id] = 2
1128 @request.session[:user_id] = 2
1030 ActionMailer::Base.deliveries.clear
1129 ActionMailer::Base.deliveries.clear
1031 post(:bulk_update,
1130 post(:bulk_update,
1032 {
1131 {
1033 :ids => [1, 2],
1132 :ids => [1, 2],
1034 :notes => 'Bulk editing',
1133 :notes => 'Bulk editing',
1035 :issue => {
1134 :issue => {
1036 :priority_id => 7,
1135 :priority_id => 7,
1037 :assigned_to_id => '',
1136 :assigned_to_id => '',
1038 :custom_field_values => {'2' => ''}
1137 :custom_field_values => {'2' => ''}
1039 }
1138 }
1040 })
1139 })
1041
1140
1042 assert_response 302
1141 assert_response 302
1043 assert_equal 2, ActionMailer::Base.deliveries.size
1142 assert_equal 2, ActionMailer::Base.deliveries.size
1044 end
1143 end
1045
1144
1046 def test_bulk_update_status
1145 def test_bulk_update_status
1047 @request.session[:user_id] = 2
1146 @request.session[:user_id] = 2
1048 # update issues priority
1147 # update issues priority
1049 post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing status',
1148 post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing status',
1050 :issue => {:priority_id => '',
1149 :issue => {:priority_id => '',
1051 :assigned_to_id => '',
1150 :assigned_to_id => '',
1052 :status_id => '5'}
1151 :status_id => '5'}
1053
1152
1054 assert_response 302
1153 assert_response 302
1055 issue = Issue.find(1)
1154 issue = Issue.find(1)
1056 assert issue.closed?
1155 assert issue.closed?
1057 end
1156 end
1058
1157
1059 def test_bulk_update_custom_field
1158 def test_bulk_update_custom_field
1060 @request.session[:user_id] = 2
1159 @request.session[:user_id] = 2
1061 # update issues priority
1160 # update issues priority
1062 post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing custom field',
1161 post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing custom field',
1063 :issue => {:priority_id => '',
1162 :issue => {:priority_id => '',
1064 :assigned_to_id => '',
1163 :assigned_to_id => '',
1065 :custom_field_values => {'2' => '777'}}
1164 :custom_field_values => {'2' => '777'}}
1066
1165
1067 assert_response 302
1166 assert_response 302
1068
1167
1069 issue = Issue.find(1)
1168 issue = Issue.find(1)
1070 journal = issue.journals.find(:first, :order => 'created_on DESC')
1169 journal = issue.journals.find(:first, :order => 'created_on DESC')
1071 assert_equal '777', issue.custom_value_for(2).value
1170 assert_equal '777', issue.custom_value_for(2).value
1072 assert_equal 1, journal.details.size
1171 assert_equal 1, journal.details.size
1073 assert_equal '125', journal.details.first.old_value
1172 assert_equal '125', journal.details.first.old_value
1074 assert_equal '777', journal.details.first.value
1173 assert_equal '777', journal.details.first.value
1075 end
1174 end
1076
1175
1077 def test_bulk_update_unassign
1176 def test_bulk_update_unassign
1078 assert_not_nil Issue.find(2).assigned_to
1177 assert_not_nil Issue.find(2).assigned_to
1079 @request.session[:user_id] = 2
1178 @request.session[:user_id] = 2
1080 # unassign issues
1179 # unassign issues
1081 post :bulk_update, :ids => [1, 2], :notes => 'Bulk unassigning', :issue => {:assigned_to_id => 'none'}
1180 post :bulk_update, :ids => [1, 2], :notes => 'Bulk unassigning', :issue => {:assigned_to_id => 'none'}
1082 assert_response 302
1181 assert_response 302
1083 # check that the issues were updated
1182 # check that the issues were updated
1084 assert_nil Issue.find(2).assigned_to
1183 assert_nil Issue.find(2).assigned_to
1085 end
1184 end
1086
1185
1087 def test_post_bulk_update_should_allow_fixed_version_to_be_set_to_a_subproject
1186 def test_post_bulk_update_should_allow_fixed_version_to_be_set_to_a_subproject
1088 @request.session[:user_id] = 2
1187 @request.session[:user_id] = 2
1089
1188
1090 post :bulk_update, :ids => [1,2], :issue => {:fixed_version_id => 4}
1189 post :bulk_update, :ids => [1,2], :issue => {:fixed_version_id => 4}
1091
1190
1092 assert_response :redirect
1191 assert_response :redirect
1093 issues = Issue.find([1,2])
1192 issues = Issue.find([1,2])
1094 issues.each do |issue|
1193 issues.each do |issue|
1095 assert_equal 4, issue.fixed_version_id
1194 assert_equal 4, issue.fixed_version_id
1096 assert_not_equal issue.project_id, issue.fixed_version.project_id
1195 assert_not_equal issue.project_id, issue.fixed_version.project_id
1097 end
1196 end
1098 end
1197 end
1099
1198
1100 def test_post_bulk_update_should_redirect_back_using_the_back_url_parameter
1199 def test_post_bulk_update_should_redirect_back_using_the_back_url_parameter
1101 @request.session[:user_id] = 2
1200 @request.session[:user_id] = 2
1102 post :bulk_update, :ids => [1,2], :back_url => '/issues'
1201 post :bulk_update, :ids => [1,2], :back_url => '/issues'
1103
1202
1104 assert_response :redirect
1203 assert_response :redirect
1105 assert_redirected_to '/issues'
1204 assert_redirected_to '/issues'
1106 end
1205 end
1107
1206
1108 def test_post_bulk_update_should_not_redirect_back_using_the_back_url_parameter_off_the_host
1207 def test_post_bulk_update_should_not_redirect_back_using_the_back_url_parameter_off_the_host
1109 @request.session[:user_id] = 2
1208 @request.session[:user_id] = 2
1110 post :bulk_update, :ids => [1,2], :back_url => 'http://google.com'
1209 post :bulk_update, :ids => [1,2], :back_url => 'http://google.com'
1111
1210
1112 assert_response :redirect
1211 assert_response :redirect
1113 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => Project.find(1).identifier
1212 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => Project.find(1).identifier
1114 end
1213 end
1115
1214
1116 def test_destroy_issue_with_no_time_entries
1215 def test_destroy_issue_with_no_time_entries
1117 assert_nil TimeEntry.find_by_issue_id(2)
1216 assert_nil TimeEntry.find_by_issue_id(2)
1118 @request.session[:user_id] = 2
1217 @request.session[:user_id] = 2
1119 post :destroy, :id => 2
1218 post :destroy, :id => 2
1120 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1219 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1121 assert_nil Issue.find_by_id(2)
1220 assert_nil Issue.find_by_id(2)
1122 end
1221 end
1123
1222
1124 def test_destroy_issues_with_time_entries
1223 def test_destroy_issues_with_time_entries
1125 @request.session[:user_id] = 2
1224 @request.session[:user_id] = 2
1126 post :destroy, :ids => [1, 3]
1225 post :destroy, :ids => [1, 3]
1127 assert_response :success
1226 assert_response :success
1128 assert_template 'destroy'
1227 assert_template 'destroy'
1129 assert_not_nil assigns(:hours)
1228 assert_not_nil assigns(:hours)
1130 assert Issue.find_by_id(1) && Issue.find_by_id(3)
1229 assert Issue.find_by_id(1) && Issue.find_by_id(3)
1131 end
1230 end
1132
1231
1133 def test_destroy_issues_and_destroy_time_entries
1232 def test_destroy_issues_and_destroy_time_entries
1134 @request.session[:user_id] = 2
1233 @request.session[:user_id] = 2
1135 post :destroy, :ids => [1, 3], :todo => 'destroy'
1234 post :destroy, :ids => [1, 3], :todo => 'destroy'
1136 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1235 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1137 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1236 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1138 assert_nil TimeEntry.find_by_id([1, 2])
1237 assert_nil TimeEntry.find_by_id([1, 2])
1139 end
1238 end
1140
1239
1141 def test_destroy_issues_and_assign_time_entries_to_project
1240 def test_destroy_issues_and_assign_time_entries_to_project
1142 @request.session[:user_id] = 2
1241 @request.session[:user_id] = 2
1143 post :destroy, :ids => [1, 3], :todo => 'nullify'
1242 post :destroy, :ids => [1, 3], :todo => 'nullify'
1144 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1243 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1145 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1244 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1146 assert_nil TimeEntry.find(1).issue_id
1245 assert_nil TimeEntry.find(1).issue_id
1147 assert_nil TimeEntry.find(2).issue_id
1246 assert_nil TimeEntry.find(2).issue_id
1148 end
1247 end
1149
1248
1150 def test_destroy_issues_and_reassign_time_entries_to_another_issue
1249 def test_destroy_issues_and_reassign_time_entries_to_another_issue
1151 @request.session[:user_id] = 2
1250 @request.session[:user_id] = 2
1152 post :destroy, :ids => [1, 3], :todo => 'reassign', :reassign_to_id => 2
1251 post :destroy, :ids => [1, 3], :todo => 'reassign', :reassign_to_id => 2
1153 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1252 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1154 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1253 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1155 assert_equal 2, TimeEntry.find(1).issue_id
1254 assert_equal 2, TimeEntry.find(1).issue_id
1156 assert_equal 2, TimeEntry.find(2).issue_id
1255 assert_equal 2, TimeEntry.find(2).issue_id
1157 end
1256 end
1158
1257
1159 def test_destroy_issues_from_different_projects
1258 def test_destroy_issues_from_different_projects
1160 @request.session[:user_id] = 2
1259 @request.session[:user_id] = 2
1161 post :destroy, :ids => [1, 2, 6], :todo => 'destroy'
1260 post :destroy, :ids => [1, 2, 6], :todo => 'destroy'
1162 assert_redirected_to :controller => 'issues', :action => 'index'
1261 assert_redirected_to :controller => 'issues', :action => 'index'
1163 assert !(Issue.find_by_id(1) || Issue.find_by_id(2) || Issue.find_by_id(6))
1262 assert !(Issue.find_by_id(1) || Issue.find_by_id(2) || Issue.find_by_id(6))
1164 end
1263 end
1165
1264
1166 def test_default_search_scope
1265 def test_default_search_scope
1167 get :index
1266 get :index
1168 assert_tag :div, :attributes => {:id => 'quick-search'},
1267 assert_tag :div, :attributes => {:id => 'quick-search'},
1169 :child => {:tag => 'form',
1268 :child => {:tag => 'form',
1170 :child => {:tag => 'input', :attributes => {:name => 'issues', :type => 'hidden', :value => '1'}}}
1269 :child => {:tag => 'input', :attributes => {:name => 'issues', :type => 'hidden', :value => '1'}}}
1171 end
1270 end
1172 end
1271 end
General Comments 0
You need to be logged in to leave comments. Login now