##// END OF EJS Templates
Bulk edit refactoring....
Jean-Philippe Lang -
r3364:e24d6cc2237d
parent child
Show More
@@ -1,575 +1,562
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
19 menu_item :new_issue, :only => :new
20 default_search_scope :issues
20 default_search_scope :issues
21
21
22 before_filter :find_issue, :only => [:show, :edit, :reply]
22 before_filter :find_issue, :only => [:show, :edit, :reply]
23 before_filter :find_issues, :only => [:bulk_edit, :move, :destroy]
23 before_filter :find_issues, :only => [:bulk_edit, :move, :destroy]
24 before_filter :find_project, :only => [:new, :update_form, :preview]
24 before_filter :find_project, :only => [:new, :update_form, :preview]
25 before_filter :authorize, :except => [:index, :changes, :gantt, :calendar, :preview, :context_menu]
25 before_filter :authorize, :except => [:index, :changes, :gantt, :calendar, :preview, :context_menu]
26 before_filter :find_optional_project, :only => [:index, :changes, :gantt, :calendar]
26 before_filter :find_optional_project, :only => [:index, :changes, :gantt, :calendar]
27 accept_key_auth :index, :show, :changes
27 accept_key_auth :index, :show, :changes
28
28
29 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
29 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
30
30
31 helper :journals
31 helper :journals
32 helper :projects
32 helper :projects
33 include ProjectsHelper
33 include ProjectsHelper
34 helper :custom_fields
34 helper :custom_fields
35 include CustomFieldsHelper
35 include CustomFieldsHelper
36 helper :issue_relations
36 helper :issue_relations
37 include IssueRelationsHelper
37 include IssueRelationsHelper
38 helper :watchers
38 helper :watchers
39 include WatchersHelper
39 include WatchersHelper
40 helper :attachments
40 helper :attachments
41 include AttachmentsHelper
41 include AttachmentsHelper
42 helper :queries
42 helper :queries
43 include QueriesHelper
43 include QueriesHelper
44 helper :sort
44 helper :sort
45 include SortHelper
45 include SortHelper
46 include IssuesHelper
46 include IssuesHelper
47 helper :timelog
47 helper :timelog
48 include Redmine::Export::PDF
48 include Redmine::Export::PDF
49
49
50 verify :method => [:post, :delete],
50 verify :method => [:post, :delete],
51 :only => :destroy,
51 :only => :destroy,
52 :render => { :nothing => true, :status => :method_not_allowed }
52 :render => { :nothing => true, :status => :method_not_allowed }
53
53
54 def index
54 def index
55 retrieve_query
55 retrieve_query
56 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
56 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
57 sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.available_columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
57 sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.available_columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
58
58
59 if @query.valid?
59 if @query.valid?
60 limit = case params[:format]
60 limit = case params[:format]
61 when 'csv', 'pdf'
61 when 'csv', 'pdf'
62 Setting.issues_export_limit.to_i
62 Setting.issues_export_limit.to_i
63 when 'atom'
63 when 'atom'
64 Setting.feeds_limit.to_i
64 Setting.feeds_limit.to_i
65 else
65 else
66 per_page_option
66 per_page_option
67 end
67 end
68
68
69 @issue_count = @query.issue_count
69 @issue_count = @query.issue_count
70 @issue_pages = Paginator.new self, @issue_count, limit, params['page']
70 @issue_pages = Paginator.new self, @issue_count, limit, params['page']
71 @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
71 @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
72 :order => sort_clause,
72 :order => sort_clause,
73 :offset => @issue_pages.current.offset,
73 :offset => @issue_pages.current.offset,
74 :limit => limit)
74 :limit => limit)
75 @issue_count_by_group = @query.issue_count_by_group
75 @issue_count_by_group = @query.issue_count_by_group
76
76
77 respond_to do |format|
77 respond_to do |format|
78 format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
78 format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
79 format.xml { render :layout => false }
79 format.xml { render :layout => false }
80 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
80 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
81 format.csv { send_data(issues_to_csv(@issues, @project), :type => 'text/csv; header=present', :filename => 'export.csv') }
81 format.csv { send_data(issues_to_csv(@issues, @project), :type => 'text/csv; header=present', :filename => 'export.csv') }
82 format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'export.pdf') }
82 format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'export.pdf') }
83 end
83 end
84 else
84 else
85 # Send html if the query is not valid
85 # Send html if the query is not valid
86 render(:template => 'issues/index.rhtml', :layout => !request.xhr?)
86 render(:template => 'issues/index.rhtml', :layout => !request.xhr?)
87 end
87 end
88 rescue ActiveRecord::RecordNotFound
88 rescue ActiveRecord::RecordNotFound
89 render_404
89 render_404
90 end
90 end
91
91
92 def changes
92 def changes
93 retrieve_query
93 retrieve_query
94 sort_init 'id', 'desc'
94 sort_init 'id', 'desc'
95 sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.available_columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
95 sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.available_columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
96
96
97 if @query.valid?
97 if @query.valid?
98 @journals = @query.journals(:order => "#{Journal.table_name}.created_on DESC",
98 @journals = @query.journals(:order => "#{Journal.table_name}.created_on DESC",
99 :limit => 25)
99 :limit => 25)
100 end
100 end
101 @title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
101 @title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
102 render :layout => false, :content_type => 'application/atom+xml'
102 render :layout => false, :content_type => 'application/atom+xml'
103 rescue ActiveRecord::RecordNotFound
103 rescue ActiveRecord::RecordNotFound
104 render_404
104 render_404
105 end
105 end
106
106
107 def show
107 def show
108 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
108 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
109 @journals.each_with_index {|j,i| j.indice = i+1}
109 @journals.each_with_index {|j,i| j.indice = i+1}
110 @journals.reverse! if User.current.wants_comments_in_reverse_order?
110 @journals.reverse! if User.current.wants_comments_in_reverse_order?
111 @changesets = @issue.changesets.visible.all
111 @changesets = @issue.changesets.visible.all
112 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
112 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
113 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
113 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
114 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
114 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
115 @priorities = IssuePriority.all
115 @priorities = IssuePriority.all
116 @time_entry = TimeEntry.new
116 @time_entry = TimeEntry.new
117 respond_to do |format|
117 respond_to do |format|
118 format.html { render :template => 'issues/show.rhtml' }
118 format.html { render :template => 'issues/show.rhtml' }
119 format.xml { render :layout => false }
119 format.xml { render :layout => false }
120 format.atom { render :action => 'changes', :layout => false, :content_type => 'application/atom+xml' }
120 format.atom { render :action => 'changes', :layout => false, :content_type => 'application/atom+xml' }
121 format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
121 format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
122 end
122 end
123 end
123 end
124
124
125 # Add a new issue
125 # Add a new issue
126 # The new issue will be created from an existing one if copy_from parameter is given
126 # The new issue will be created from an existing one if copy_from parameter is given
127 def new
127 def new
128 @issue = Issue.new
128 @issue = Issue.new
129 @issue.copy_from(params[:copy_from]) if params[:copy_from]
129 @issue.copy_from(params[:copy_from]) if params[:copy_from]
130 @issue.project = @project
130 @issue.project = @project
131 # Tracker must be set before custom field values
131 # Tracker must be set before custom field values
132 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
132 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
133 if @issue.tracker.nil?
133 if @issue.tracker.nil?
134 render_error l(:error_no_tracker_in_project)
134 render_error l(:error_no_tracker_in_project)
135 return
135 return
136 end
136 end
137 if params[:issue].is_a?(Hash)
137 if params[:issue].is_a?(Hash)
138 @issue.safe_attributes = params[:issue]
138 @issue.safe_attributes = params[:issue]
139 @issue.watcher_user_ids = params[:issue]['watcher_user_ids'] if User.current.allowed_to?(:add_issue_watchers, @project)
139 @issue.watcher_user_ids = params[:issue]['watcher_user_ids'] if User.current.allowed_to?(:add_issue_watchers, @project)
140 end
140 end
141 @issue.author = User.current
141 @issue.author = User.current
142
142
143 default_status = IssueStatus.default
143 default_status = IssueStatus.default
144 unless default_status
144 unless default_status
145 render_error l(:error_no_default_issue_status)
145 render_error l(:error_no_default_issue_status)
146 return
146 return
147 end
147 end
148 @issue.status = default_status
148 @issue.status = default_status
149 @allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(User.current.roles_for_project(@project), @issue.tracker)).uniq
149 @allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(User.current.roles_for_project(@project), @issue.tracker)).uniq
150
150
151 if request.get? || request.xhr?
151 if request.get? || request.xhr?
152 @issue.start_date ||= Date.today
152 @issue.start_date ||= Date.today
153 else
153 else
154 requested_status = IssueStatus.find_by_id(params[:issue][:status_id])
154 requested_status = IssueStatus.find_by_id(params[:issue][:status_id])
155 # Check that the user is allowed to apply the requested status
155 # Check that the user is allowed to apply the requested status
156 @issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status
156 @issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status
157 call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
157 call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
158 if @issue.save
158 if @issue.save
159 attach_files(@issue, params[:attachments])
159 attach_files(@issue, params[:attachments])
160 flash[:notice] = l(:notice_successful_create)
160 flash[:notice] = l(:notice_successful_create)
161 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
161 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
162 respond_to do |format|
162 respond_to do |format|
163 format.html {
163 format.html {
164 redirect_to(params[:continue] ? { :action => 'new', :tracker_id => @issue.tracker } :
164 redirect_to(params[:continue] ? { :action => 'new', :tracker_id => @issue.tracker } :
165 { :action => 'show', :id => @issue })
165 { :action => 'show', :id => @issue })
166 }
166 }
167 format.xml { render :action => 'show', :status => :created, :location => url_for(:controller => 'issues', :action => 'show', :id => @issue) }
167 format.xml { render :action => 'show', :status => :created, :location => url_for(:controller => 'issues', :action => 'show', :id => @issue) }
168 end
168 end
169 return
169 return
170 else
170 else
171 respond_to do |format|
171 respond_to do |format|
172 format.html { }
172 format.html { }
173 format.xml { render(:xml => @issue.errors, :status => :unprocessable_entity); return }
173 format.xml { render(:xml => @issue.errors, :status => :unprocessable_entity); return }
174 end
174 end
175 end
175 end
176 end
176 end
177 @priorities = IssuePriority.all
177 @priorities = IssuePriority.all
178 render :layout => !request.xhr?
178 render :layout => !request.xhr?
179 end
179 end
180
180
181 # Attributes that can be updated on workflow transition (without :edit permission)
181 # Attributes that can be updated on workflow transition (without :edit permission)
182 # TODO: make it configurable (at least per role)
182 # TODO: make it configurable (at least per role)
183 UPDATABLE_ATTRS_ON_TRANSITION = %w(status_id assigned_to_id fixed_version_id done_ratio) unless const_defined?(:UPDATABLE_ATTRS_ON_TRANSITION)
183 UPDATABLE_ATTRS_ON_TRANSITION = %w(status_id assigned_to_id fixed_version_id done_ratio) unless const_defined?(:UPDATABLE_ATTRS_ON_TRANSITION)
184
184
185 def edit
185 def edit
186 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
186 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
187 @priorities = IssuePriority.all
187 @priorities = IssuePriority.all
188 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
188 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
189 @time_entry = TimeEntry.new
189 @time_entry = TimeEntry.new
190
190
191 @notes = params[:notes]
191 @notes = params[:notes]
192 journal = @issue.init_journal(User.current, @notes)
192 journal = @issue.init_journal(User.current, @notes)
193 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
193 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
194 if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue]
194 if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue]
195 attrs = params[:issue].dup
195 attrs = params[:issue].dup
196 attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed
196 attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed
197 attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s}
197 attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s}
198 @issue.safe_attributes = attrs
198 @issue.safe_attributes = attrs
199 end
199 end
200
200
201 if request.get?
201 if request.get?
202 # nop
202 # nop
203 else
203 else
204 @time_entry = TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
204 @time_entry = TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
205 @time_entry.attributes = params[:time_entry]
205 @time_entry.attributes = params[:time_entry]
206 if (@time_entry.hours.nil? || @time_entry.valid?) && @issue.valid?
206 if (@time_entry.hours.nil? || @time_entry.valid?) && @issue.valid?
207 attachments = attach_files(@issue, params[:attachments])
207 attachments = attach_files(@issue, params[:attachments])
208 attachments.each {|a| journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
208 attachments.each {|a| journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
209 call_hook(:controller_issues_edit_before_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
209 call_hook(:controller_issues_edit_before_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
210 if @issue.save
210 if @issue.save
211 # Log spend time
211 # Log spend time
212 if User.current.allowed_to?(:log_time, @project)
212 if User.current.allowed_to?(:log_time, @project)
213 @time_entry.save
213 @time_entry.save
214 end
214 end
215 if !journal.new_record?
215 if !journal.new_record?
216 # Only send notification if something was actually changed
216 # Only send notification if something was actually changed
217 flash[:notice] = l(:notice_successful_update)
217 flash[:notice] = l(:notice_successful_update)
218 end
218 end
219 call_hook(:controller_issues_edit_after_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
219 call_hook(:controller_issues_edit_after_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
220 respond_to do |format|
220 respond_to do |format|
221 format.html { redirect_back_or_default({:action => 'show', :id => @issue}) }
221 format.html { redirect_back_or_default({:action => 'show', :id => @issue}) }
222 format.xml { head :ok }
222 format.xml { head :ok }
223 end
223 end
224 return
224 return
225 end
225 end
226 end
226 end
227 # failure
227 # failure
228 respond_to do |format|
228 respond_to do |format|
229 format.html { }
229 format.html { }
230 format.xml { render :xml => @issue.errors, :status => :unprocessable_entity }
230 format.xml { render :xml => @issue.errors, :status => :unprocessable_entity }
231 end
231 end
232 end
232 end
233 rescue ActiveRecord::StaleObjectError
233 rescue ActiveRecord::StaleObjectError
234 # Optimistic locking exception
234 # Optimistic locking exception
235 flash.now[:error] = l(:notice_locking_conflict)
235 flash.now[:error] = l(:notice_locking_conflict)
236 # Remove the previously added attachments if issue was not updated
236 # Remove the previously added attachments if issue was not updated
237 attachments.each(&:destroy)
237 attachments.each(&:destroy)
238 end
238 end
239
239
240 def reply
240 def reply
241 journal = Journal.find(params[:journal_id]) if params[:journal_id]
241 journal = Journal.find(params[:journal_id]) if params[:journal_id]
242 if journal
242 if journal
243 user = journal.user
243 user = journal.user
244 text = journal.notes
244 text = journal.notes
245 else
245 else
246 user = @issue.author
246 user = @issue.author
247 text = @issue.description
247 text = @issue.description
248 end
248 end
249 content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
249 content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
250 content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
250 content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
251 render(:update) { |page|
251 render(:update) { |page|
252 page.<< "$('notes').value = \"#{content}\";"
252 page.<< "$('notes').value = \"#{content}\";"
253 page.show 'update'
253 page.show 'update'
254 page << "Form.Element.focus('notes');"
254 page << "Form.Element.focus('notes');"
255 page << "Element.scrollTo('update');"
255 page << "Element.scrollTo('update');"
256 page << "$('notes').scrollTop = $('notes').scrollHeight - $('notes').clientHeight;"
256 page << "$('notes').scrollTop = $('notes').scrollHeight - $('notes').clientHeight;"
257 }
257 }
258 end
258 end
259
259
260 # Bulk edit a set of issues
260 # Bulk edit a set of issues
261 def bulk_edit
261 def bulk_edit
262 if request.post?
262 if request.post?
263 tracker = params[:tracker_id].blank? ? nil : @project.trackers.find_by_id(params[:tracker_id])
263 attributes = (params[:issue] || {}).reject {|k,v| v.blank?}
264 status = params[:status_id].blank? ? nil : IssueStatus.find_by_id(params[:status_id])
264 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
265 priority = params[:priority_id].blank? ? nil : IssuePriority.find_by_id(params[:priority_id])
265 attributes[:custom_field_values].reject! {|k,v| v.blank?} if attributes[:custom_field_values]
266 assigned_to = (params[:assigned_to_id].blank? || params[:assigned_to_id] == 'none') ? nil : User.find_by_id(params[:assigned_to_id])
267 category = (params[:category_id].blank? || params[:category_id] == 'none') ? nil : @project.issue_categories.find_by_id(params[:category_id])
268 fixed_version = (params[:fixed_version_id].blank? || params[:fixed_version_id] == 'none') ? nil : @project.shared_versions.find_by_id(params[:fixed_version_id])
269 custom_field_values = params[:custom_field_values] ? params[:custom_field_values].reject {|k,v| v.blank?} : nil
270
271 # Need to merge in the records found above for Issue#bulk_edit.
272 # Assuming this is done so the associations are only looked up once.
273 merged_params = params.merge({
274 :tracker => tracker,
275 :status => status,
276 :priority => priority,
277 :assigned_to => assigned_to,
278 :category => category,
279 :fixed_version => fixed_version,
280 :custom_field_values => custom_field_values
281 })
282
266
283 unsaved_issue_ids = []
267 unsaved_issue_ids = []
284 @issues.each do |issue|
268 @issues.each do |issue|
285 unless issue.bulk_edit(merged_params)
269 journal = issue.init_journal(User.current, params[:notes])
270 issue.safe_attributes = attributes
271 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
272 unless issue.save
286 # Keep unsaved issue ids to display them in flash error
273 # Keep unsaved issue ids to display them in flash error
287 unsaved_issue_ids << issue.id
274 unsaved_issue_ids << issue.id
288 end
275 end
289 end
276 end
290 if unsaved_issue_ids.empty?
277 if unsaved_issue_ids.empty?
291 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
278 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
292 else
279 else
293 flash[:error] = l(:notice_failed_to_save_issues, :count => unsaved_issue_ids.size,
280 flash[:error] = l(:notice_failed_to_save_issues, :count => unsaved_issue_ids.size,
294 :total => @issues.size,
281 :total => @issues.size,
295 :ids => '#' + unsaved_issue_ids.join(', #'))
282 :ids => '#' + unsaved_issue_ids.join(', #'))
296 end
283 end
297 redirect_back_or_default({:controller => 'issues', :action => 'index', :project_id => @project})
284 redirect_back_or_default({:controller => 'issues', :action => 'index', :project_id => @project})
298 return
285 return
299 end
286 end
300 @available_statuses = Workflow.available_statuses(@project)
287 @available_statuses = Workflow.available_statuses(@project)
301 @custom_fields = @project.all_issue_custom_fields
288 @custom_fields = @project.all_issue_custom_fields
302 end
289 end
303
290
304 def move
291 def move
305 @copy = params[:copy_options] && params[:copy_options][:copy]
292 @copy = params[:copy_options] && params[:copy_options][:copy]
306 @allowed_projects = []
293 @allowed_projects = []
307 # find projects to which the user is allowed to move the issue
294 # find projects to which the user is allowed to move the issue
308 if User.current.admin?
295 if User.current.admin?
309 # admin is allowed to move issues to any active (visible) project
296 # admin is allowed to move issues to any active (visible) project
310 @allowed_projects = Project.find(:all, :conditions => Project.visible_by(User.current))
297 @allowed_projects = Project.find(:all, :conditions => Project.visible_by(User.current))
311 else
298 else
312 User.current.memberships.each {|m| @allowed_projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}}
299 User.current.memberships.each {|m| @allowed_projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}}
313 end
300 end
314 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id]
301 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id]
315 @target_project ||= @project
302 @target_project ||= @project
316 @trackers = @target_project.trackers
303 @trackers = @target_project.trackers
317 @available_statuses = Workflow.available_statuses(@project)
304 @available_statuses = Workflow.available_statuses(@project)
318 if request.post?
305 if request.post?
319 new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id])
306 new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id])
320 unsaved_issue_ids = []
307 unsaved_issue_ids = []
321 moved_issues = []
308 moved_issues = []
322 @issues.each do |issue|
309 @issues.each do |issue|
323 changed_attributes = {}
310 changed_attributes = {}
324 [:assigned_to_id, :status_id, :start_date, :due_date].each do |valid_attribute|
311 [:assigned_to_id, :status_id, :start_date, :due_date].each do |valid_attribute|
325 unless params[valid_attribute].blank?
312 unless params[valid_attribute].blank?
326 changed_attributes[valid_attribute] = (params[valid_attribute] == 'none' ? nil : params[valid_attribute])
313 changed_attributes[valid_attribute] = (params[valid_attribute] == 'none' ? nil : params[valid_attribute])
327 end
314 end
328 end
315 end
329 issue.init_journal(User.current)
316 issue.init_journal(User.current)
330 call_hook(:controller_issues_move_before_save, { :params => params, :issue => issue, :target_project => @target_project, :copy => !!@copy })
317 call_hook(:controller_issues_move_before_save, { :params => params, :issue => issue, :target_project => @target_project, :copy => !!@copy })
331 if r = issue.move_to(@target_project, new_tracker, {:copy => @copy, :attributes => changed_attributes})
318 if r = issue.move_to(@target_project, new_tracker, {:copy => @copy, :attributes => changed_attributes})
332 moved_issues << r
319 moved_issues << r
333 else
320 else
334 unsaved_issue_ids << issue.id
321 unsaved_issue_ids << issue.id
335 end
322 end
336 end
323 end
337 if unsaved_issue_ids.empty?
324 if unsaved_issue_ids.empty?
338 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
325 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
339 else
326 else
340 flash[:error] = l(:notice_failed_to_save_issues, :count => unsaved_issue_ids.size,
327 flash[:error] = l(:notice_failed_to_save_issues, :count => unsaved_issue_ids.size,
341 :total => @issues.size,
328 :total => @issues.size,
342 :ids => '#' + unsaved_issue_ids.join(', #'))
329 :ids => '#' + unsaved_issue_ids.join(', #'))
343 end
330 end
344 if params[:follow]
331 if params[:follow]
345 if @issues.size == 1 && moved_issues.size == 1
332 if @issues.size == 1 && moved_issues.size == 1
346 redirect_to :controller => 'issues', :action => 'show', :id => moved_issues.first
333 redirect_to :controller => 'issues', :action => 'show', :id => moved_issues.first
347 else
334 else
348 redirect_to :controller => 'issues', :action => 'index', :project_id => (@target_project || @project)
335 redirect_to :controller => 'issues', :action => 'index', :project_id => (@target_project || @project)
349 end
336 end
350 else
337 else
351 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
338 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
352 end
339 end
353 return
340 return
354 end
341 end
355 render :layout => false if request.xhr?
342 render :layout => false if request.xhr?
356 end
343 end
357
344
358 def destroy
345 def destroy
359 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
346 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
360 if @hours > 0
347 if @hours > 0
361 case params[:todo]
348 case params[:todo]
362 when 'destroy'
349 when 'destroy'
363 # nothing to do
350 # nothing to do
364 when 'nullify'
351 when 'nullify'
365 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
352 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
366 when 'reassign'
353 when 'reassign'
367 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
354 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
368 if reassign_to.nil?
355 if reassign_to.nil?
369 flash.now[:error] = l(:error_issue_not_found_in_project)
356 flash.now[:error] = l(:error_issue_not_found_in_project)
370 return
357 return
371 else
358 else
372 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
359 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
373 end
360 end
374 else
361 else
375 unless params[:format] == 'xml'
362 unless params[:format] == 'xml'
376 # display the destroy form if it's a user request
363 # display the destroy form if it's a user request
377 return
364 return
378 end
365 end
379 end
366 end
380 end
367 end
381 @issues.each(&:destroy)
368 @issues.each(&:destroy)
382 respond_to do |format|
369 respond_to do |format|
383 format.html { redirect_to :action => 'index', :project_id => @project }
370 format.html { redirect_to :action => 'index', :project_id => @project }
384 format.xml { head :ok }
371 format.xml { head :ok }
385 end
372 end
386 end
373 end
387
374
388 def gantt
375 def gantt
389 @gantt = Redmine::Helpers::Gantt.new(params)
376 @gantt = Redmine::Helpers::Gantt.new(params)
390 retrieve_query
377 retrieve_query
391 @query.group_by = nil
378 @query.group_by = nil
392 if @query.valid?
379 if @query.valid?
393 events = []
380 events = []
394 # Issues that have start and due dates
381 # Issues that have start and due dates
395 events += @query.issues(:include => [:tracker, :assigned_to, :priority],
382 events += @query.issues(:include => [:tracker, :assigned_to, :priority],
396 :order => "start_date, due_date",
383 :order => "start_date, due_date",
397 :conditions => ["(((start_date>=? and start_date<=?) or (due_date>=? and due_date<=?) or (start_date<? and due_date>?)) and start_date is not null and due_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to]
384 :conditions => ["(((start_date>=? and start_date<=?) or (due_date>=? and due_date<=?) or (start_date<? and due_date>?)) and start_date is not null and due_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to]
398 )
385 )
399 # Issues that don't have a due date but that are assigned to a version with a date
386 # Issues that don't have a due date but that are assigned to a version with a date
400 events += @query.issues(:include => [:tracker, :assigned_to, :priority, :fixed_version],
387 events += @query.issues(:include => [:tracker, :assigned_to, :priority, :fixed_version],
401 :order => "start_date, effective_date",
388 :order => "start_date, effective_date",
402 :conditions => ["(((start_date>=? and start_date<=?) or (effective_date>=? and effective_date<=?) or (start_date<? and effective_date>?)) and start_date is not null and due_date is null and effective_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to]
389 :conditions => ["(((start_date>=? and start_date<=?) or (effective_date>=? and effective_date<=?) or (start_date<? and effective_date>?)) and start_date is not null and due_date is null and effective_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to]
403 )
390 )
404 # Versions
391 # Versions
405 events += @query.versions(:conditions => ["effective_date BETWEEN ? AND ?", @gantt.date_from, @gantt.date_to])
392 events += @query.versions(:conditions => ["effective_date BETWEEN ? AND ?", @gantt.date_from, @gantt.date_to])
406
393
407 @gantt.events = events
394 @gantt.events = events
408 end
395 end
409
396
410 basename = (@project ? "#{@project.identifier}-" : '') + 'gantt'
397 basename = (@project ? "#{@project.identifier}-" : '') + 'gantt'
411
398
412 respond_to do |format|
399 respond_to do |format|
413 format.html { render :template => "issues/gantt.rhtml", :layout => !request.xhr? }
400 format.html { render :template => "issues/gantt.rhtml", :layout => !request.xhr? }
414 format.png { send_data(@gantt.to_image, :disposition => 'inline', :type => 'image/png', :filename => "#{basename}.png") } if @gantt.respond_to?('to_image')
401 format.png { send_data(@gantt.to_image, :disposition => 'inline', :type => 'image/png', :filename => "#{basename}.png") } if @gantt.respond_to?('to_image')
415 format.pdf { send_data(gantt_to_pdf(@gantt, @project), :type => 'application/pdf', :filename => "#{basename}.pdf") }
402 format.pdf { send_data(gantt_to_pdf(@gantt, @project), :type => 'application/pdf', :filename => "#{basename}.pdf") }
416 end
403 end
417 end
404 end
418
405
419 def calendar
406 def calendar
420 if params[:year] and params[:year].to_i > 1900
407 if params[:year] and params[:year].to_i > 1900
421 @year = params[:year].to_i
408 @year = params[:year].to_i
422 if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
409 if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
423 @month = params[:month].to_i
410 @month = params[:month].to_i
424 end
411 end
425 end
412 end
426 @year ||= Date.today.year
413 @year ||= Date.today.year
427 @month ||= Date.today.month
414 @month ||= Date.today.month
428
415
429 @calendar = Redmine::Helpers::Calendar.new(Date.civil(@year, @month, 1), current_language, :month)
416 @calendar = Redmine::Helpers::Calendar.new(Date.civil(@year, @month, 1), current_language, :month)
430 retrieve_query
417 retrieve_query
431 @query.group_by = nil
418 @query.group_by = nil
432 if @query.valid?
419 if @query.valid?
433 events = []
420 events = []
434 events += @query.issues(:include => [:tracker, :assigned_to, :priority],
421 events += @query.issues(:include => [:tracker, :assigned_to, :priority],
435 :conditions => ["((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?))", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt]
422 :conditions => ["((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?))", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt]
436 )
423 )
437 events += @query.versions(:conditions => ["effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt])
424 events += @query.versions(:conditions => ["effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt])
438
425
439 @calendar.events = events
426 @calendar.events = events
440 end
427 end
441
428
442 render :layout => false if request.xhr?
429 render :layout => false if request.xhr?
443 end
430 end
444
431
445 def context_menu
432 def context_menu
446 @issues = Issue.find_all_by_id(params[:ids], :include => :project)
433 @issues = Issue.find_all_by_id(params[:ids], :include => :project)
447 if (@issues.size == 1)
434 if (@issues.size == 1)
448 @issue = @issues.first
435 @issue = @issues.first
449 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
436 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
450 end
437 end
451 projects = @issues.collect(&:project).compact.uniq
438 projects = @issues.collect(&:project).compact.uniq
452 @project = projects.first if projects.size == 1
439 @project = projects.first if projects.size == 1
453
440
454 @can = {:edit => (@project && User.current.allowed_to?(:edit_issues, @project)),
441 @can = {:edit => (@project && User.current.allowed_to?(:edit_issues, @project)),
455 :log_time => (@project && User.current.allowed_to?(:log_time, @project)),
442 :log_time => (@project && User.current.allowed_to?(:log_time, @project)),
456 :update => (@project && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && @allowed_statuses && !@allowed_statuses.empty?))),
443 :update => (@project && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && @allowed_statuses && !@allowed_statuses.empty?))),
457 :move => (@project && User.current.allowed_to?(:move_issues, @project)),
444 :move => (@project && User.current.allowed_to?(:move_issues, @project)),
458 :copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
445 :copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
459 :delete => (@project && User.current.allowed_to?(:delete_issues, @project))
446 :delete => (@project && User.current.allowed_to?(:delete_issues, @project))
460 }
447 }
461 if @project
448 if @project
462 @assignables = @project.assignable_users
449 @assignables = @project.assignable_users
463 @assignables << @issue.assigned_to if @issue && @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
450 @assignables << @issue.assigned_to if @issue && @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
464 @trackers = @project.trackers
451 @trackers = @project.trackers
465 end
452 end
466
453
467 @priorities = IssuePriority.all.reverse
454 @priorities = IssuePriority.all.reverse
468 @statuses = IssueStatus.find(:all, :order => 'position')
455 @statuses = IssueStatus.find(:all, :order => 'position')
469 @back = params[:back_url] || request.env['HTTP_REFERER']
456 @back = params[:back_url] || request.env['HTTP_REFERER']
470
457
471 render :layout => false
458 render :layout => false
472 end
459 end
473
460
474 def update_form
461 def update_form
475 if params[:id].blank?
462 if params[:id].blank?
476 @issue = Issue.new
463 @issue = Issue.new
477 @issue.project = @project
464 @issue.project = @project
478 else
465 else
479 @issue = @project.issues.visible.find(params[:id])
466 @issue = @project.issues.visible.find(params[:id])
480 end
467 end
481 @issue.attributes = params[:issue]
468 @issue.attributes = params[:issue]
482 @allowed_statuses = ([@issue.status] + @issue.status.find_new_statuses_allowed_to(User.current.roles_for_project(@project), @issue.tracker)).uniq
469 @allowed_statuses = ([@issue.status] + @issue.status.find_new_statuses_allowed_to(User.current.roles_for_project(@project), @issue.tracker)).uniq
483 @priorities = IssuePriority.all
470 @priorities = IssuePriority.all
484
471
485 render :partial => 'attributes'
472 render :partial => 'attributes'
486 end
473 end
487
474
488 def preview
475 def preview
489 @issue = @project.issues.find_by_id(params[:id]) unless params[:id].blank?
476 @issue = @project.issues.find_by_id(params[:id]) unless params[:id].blank?
490 @attachements = @issue.attachments if @issue
477 @attachements = @issue.attachments if @issue
491 @text = params[:notes] || (params[:issue] ? params[:issue][:description] : nil)
478 @text = params[:notes] || (params[:issue] ? params[:issue][:description] : nil)
492 render :partial => 'common/preview'
479 render :partial => 'common/preview'
493 end
480 end
494
481
495 private
482 private
496 def find_issue
483 def find_issue
497 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
484 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
498 @project = @issue.project
485 @project = @issue.project
499 rescue ActiveRecord::RecordNotFound
486 rescue ActiveRecord::RecordNotFound
500 render_404
487 render_404
501 end
488 end
502
489
503 # Filter for bulk operations
490 # Filter for bulk operations
504 def find_issues
491 def find_issues
505 @issues = Issue.find_all_by_id(params[:id] || params[:ids])
492 @issues = Issue.find_all_by_id(params[:id] || params[:ids])
506 raise ActiveRecord::RecordNotFound if @issues.empty?
493 raise ActiveRecord::RecordNotFound if @issues.empty?
507 projects = @issues.collect(&:project).compact.uniq
494 projects = @issues.collect(&:project).compact.uniq
508 if projects.size == 1
495 if projects.size == 1
509 @project = projects.first
496 @project = projects.first
510 else
497 else
511 # TODO: let users bulk edit/move/destroy issues from different projects
498 # TODO: let users bulk edit/move/destroy issues from different projects
512 render_error 'Can not bulk edit/move/destroy issues from different projects'
499 render_error 'Can not bulk edit/move/destroy issues from different projects'
513 return false
500 return false
514 end
501 end
515 rescue ActiveRecord::RecordNotFound
502 rescue ActiveRecord::RecordNotFound
516 render_404
503 render_404
517 end
504 end
518
505
519 def find_project
506 def find_project
520 project_id = (params[:issue] && params[:issue][:project_id]) || params[:project_id]
507 project_id = (params[:issue] && params[:issue][:project_id]) || params[:project_id]
521 @project = Project.find(project_id)
508 @project = Project.find(project_id)
522 rescue ActiveRecord::RecordNotFound
509 rescue ActiveRecord::RecordNotFound
523 render_404
510 render_404
524 end
511 end
525
512
526 def find_optional_project
513 def find_optional_project
527 @project = Project.find(params[:project_id]) unless params[:project_id].blank?
514 @project = Project.find(params[:project_id]) unless params[:project_id].blank?
528 allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
515 allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
529 allowed ? true : deny_access
516 allowed ? true : deny_access
530 rescue ActiveRecord::RecordNotFound
517 rescue ActiveRecord::RecordNotFound
531 render_404
518 render_404
532 end
519 end
533
520
534 # Retrieve query from session or build a new query
521 # Retrieve query from session or build a new query
535 def retrieve_query
522 def retrieve_query
536 if !params[:query_id].blank?
523 if !params[:query_id].blank?
537 cond = "project_id IS NULL"
524 cond = "project_id IS NULL"
538 cond << " OR project_id = #{@project.id}" if @project
525 cond << " OR project_id = #{@project.id}" if @project
539 @query = Query.find(params[:query_id], :conditions => cond)
526 @query = Query.find(params[:query_id], :conditions => cond)
540 @query.project = @project
527 @query.project = @project
541 session[:query] = {:id => @query.id, :project_id => @query.project_id}
528 session[:query] = {:id => @query.id, :project_id => @query.project_id}
542 sort_clear
529 sort_clear
543 else
530 else
544 if api_request? || params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
531 if api_request? || params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
545 # Give it a name, required to be valid
532 # Give it a name, required to be valid
546 @query = Query.new(:name => "_")
533 @query = Query.new(:name => "_")
547 @query.project = @project
534 @query.project = @project
548 if params[:fields] and params[:fields].is_a? Array
535 if params[:fields] and params[:fields].is_a? Array
549 params[:fields].each do |field|
536 params[:fields].each do |field|
550 @query.add_filter(field, params[:operators][field], params[:values][field])
537 @query.add_filter(field, params[:operators][field], params[:values][field])
551 end
538 end
552 else
539 else
553 @query.available_filters.keys.each do |field|
540 @query.available_filters.keys.each do |field|
554 @query.add_short_filter(field, params[field]) if params[field]
541 @query.add_short_filter(field, params[field]) if params[field]
555 end
542 end
556 end
543 end
557 @query.group_by = params[:group_by]
544 @query.group_by = params[:group_by]
558 @query.column_names = params[:query] && params[:query][:column_names]
545 @query.column_names = params[:query] && params[:query][:column_names]
559 session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names}
546 session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names}
560 else
547 else
561 @query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
548 @query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
562 @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
549 @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
563 @query.project = @project
550 @query.project = @project
564 end
551 end
565 end
552 end
566 end
553 end
567
554
568 # Rescues an invalid query statement. Just in case...
555 # Rescues an invalid query statement. Just in case...
569 def query_statement_invalid(exception)
556 def query_statement_invalid(exception)
570 logger.error "Query::StatementInvalid: #{exception.message}" if logger
557 logger.error "Query::StatementInvalid: #{exception.message}" if logger
571 session.delete(:query)
558 session.delete(:query)
572 sort_clear
559 sort_clear
573 render_error "An error occurred while executing the query and has been logged. Please report this error to your Redmine administrator."
560 render_error "An error occurred while executing the query and has been logged. Please report this error to your Redmine administrator."
574 end
561 end
575 end
562 end
@@ -1,113 +1,113
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
2 # Copyright (C) 2006 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 module CustomFieldsHelper
18 module CustomFieldsHelper
19
19
20 def custom_fields_tabs
20 def custom_fields_tabs
21 tabs = [{:name => 'IssueCustomField', :partial => 'custom_fields/index', :label => :label_issue_plural},
21 tabs = [{:name => 'IssueCustomField', :partial => 'custom_fields/index', :label => :label_issue_plural},
22 {:name => 'TimeEntryCustomField', :partial => 'custom_fields/index', :label => :label_spent_time},
22 {:name => 'TimeEntryCustomField', :partial => 'custom_fields/index', :label => :label_spent_time},
23 {:name => 'ProjectCustomField', :partial => 'custom_fields/index', :label => :label_project_plural},
23 {:name => 'ProjectCustomField', :partial => 'custom_fields/index', :label => :label_project_plural},
24 {:name => 'VersionCustomField', :partial => 'custom_fields/index', :label => :label_version_plural},
24 {:name => 'VersionCustomField', :partial => 'custom_fields/index', :label => :label_version_plural},
25 {:name => 'UserCustomField', :partial => 'custom_fields/index', :label => :label_user_plural},
25 {:name => 'UserCustomField', :partial => 'custom_fields/index', :label => :label_user_plural},
26 {:name => 'GroupCustomField', :partial => 'custom_fields/index', :label => :label_group_plural},
26 {:name => 'GroupCustomField', :partial => 'custom_fields/index', :label => :label_group_plural},
27 {:name => 'TimeEntryActivityCustomField', :partial => 'custom_fields/index', :label => TimeEntryActivity::OptionName},
27 {:name => 'TimeEntryActivityCustomField', :partial => 'custom_fields/index', :label => TimeEntryActivity::OptionName},
28 {:name => 'IssuePriorityCustomField', :partial => 'custom_fields/index', :label => IssuePriority::OptionName},
28 {:name => 'IssuePriorityCustomField', :partial => 'custom_fields/index', :label => IssuePriority::OptionName},
29 {:name => 'DocumentCategoryCustomField', :partial => 'custom_fields/index', :label => DocumentCategory::OptionName}
29 {:name => 'DocumentCategoryCustomField', :partial => 'custom_fields/index', :label => DocumentCategory::OptionName}
30 ]
30 ]
31 end
31 end
32
32
33 # Return custom field html tag corresponding to its format
33 # Return custom field html tag corresponding to its format
34 def custom_field_tag(name, custom_value)
34 def custom_field_tag(name, custom_value)
35 custom_field = custom_value.custom_field
35 custom_field = custom_value.custom_field
36 field_name = "#{name}[custom_field_values][#{custom_field.id}]"
36 field_name = "#{name}[custom_field_values][#{custom_field.id}]"
37 field_id = "#{name}_custom_field_values_#{custom_field.id}"
37 field_id = "#{name}_custom_field_values_#{custom_field.id}"
38
38
39 case custom_field.field_format
39 case custom_field.field_format
40 when "date"
40 when "date"
41 text_field_tag(field_name, custom_value.value, :id => field_id, :size => 10) +
41 text_field_tag(field_name, custom_value.value, :id => field_id, :size => 10) +
42 calendar_for(field_id)
42 calendar_for(field_id)
43 when "text"
43 when "text"
44 text_area_tag(field_name, custom_value.value, :id => field_id, :rows => 3, :style => 'width:90%')
44 text_area_tag(field_name, custom_value.value, :id => field_id, :rows => 3, :style => 'width:90%')
45 when "bool"
45 when "bool"
46 hidden_field_tag(field_name, '0') + check_box_tag(field_name, '1', custom_value.true?, :id => field_id)
46 hidden_field_tag(field_name, '0') + check_box_tag(field_name, '1', custom_value.true?, :id => field_id)
47 when "list"
47 when "list"
48 blank_option = custom_field.is_required? ?
48 blank_option = custom_field.is_required? ?
49 (custom_field.default_value.blank? ? "<option value=\"\">--- #{l(:actionview_instancetag_blank_option)} ---</option>" : '') :
49 (custom_field.default_value.blank? ? "<option value=\"\">--- #{l(:actionview_instancetag_blank_option)} ---</option>" : '') :
50 '<option></option>'
50 '<option></option>'
51 select_tag(field_name, blank_option + options_for_select(custom_field.possible_values, custom_value.value), :id => field_id)
51 select_tag(field_name, blank_option + options_for_select(custom_field.possible_values, custom_value.value), :id => field_id)
52 else
52 else
53 text_field_tag(field_name, custom_value.value, :id => field_id)
53 text_field_tag(field_name, custom_value.value, :id => field_id)
54 end
54 end
55 end
55 end
56
56
57 # Return custom field label tag
57 # Return custom field label tag
58 def custom_field_label_tag(name, custom_value)
58 def custom_field_label_tag(name, custom_value)
59 content_tag "label", custom_value.custom_field.name +
59 content_tag "label", custom_value.custom_field.name +
60 (custom_value.custom_field.is_required? ? " <span class=\"required\">*</span>" : ""),
60 (custom_value.custom_field.is_required? ? " <span class=\"required\">*</span>" : ""),
61 :for => "#{name}_custom_field_values_#{custom_value.custom_field.id}",
61 :for => "#{name}_custom_field_values_#{custom_value.custom_field.id}",
62 :class => (custom_value.errors.empty? ? nil : "error" )
62 :class => (custom_value.errors.empty? ? nil : "error" )
63 end
63 end
64
64
65 # Return custom field tag with its label tag
65 # Return custom field tag with its label tag
66 def custom_field_tag_with_label(name, custom_value)
66 def custom_field_tag_with_label(name, custom_value)
67 custom_field_label_tag(name, custom_value) + custom_field_tag(name, custom_value)
67 custom_field_label_tag(name, custom_value) + custom_field_tag(name, custom_value)
68 end
68 end
69
69
70 def custom_field_tag_for_bulk_edit(custom_field)
70 def custom_field_tag_for_bulk_edit(name, custom_field)
71 field_name = "custom_field_values[#{custom_field.id}]"
71 field_name = "#{name}[custom_field_values][#{custom_field.id}]"
72 field_id = "custom_field_values_#{custom_field.id}"
72 field_id = "#{name}_custom_field_values_#{custom_field.id}"
73 case custom_field.field_format
73 case custom_field.field_format
74 when "date"
74 when "date"
75 text_field_tag(field_name, '', :id => field_id, :size => 10) +
75 text_field_tag(field_name, '', :id => field_id, :size => 10) +
76 calendar_for(field_id)
76 calendar_for(field_id)
77 when "text"
77 when "text"
78 text_area_tag(field_name, '', :id => field_id, :rows => 3, :style => 'width:90%')
78 text_area_tag(field_name, '', :id => field_id, :rows => 3, :style => 'width:90%')
79 when "bool"
79 when "bool"
80 select_tag(field_name, options_for_select([[l(:label_no_change_option), ''],
80 select_tag(field_name, options_for_select([[l(:label_no_change_option), ''],
81 [l(:general_text_yes), '1'],
81 [l(:general_text_yes), '1'],
82 [l(:general_text_no), '0']]), :id => field_id)
82 [l(:general_text_no), '0']]), :id => field_id)
83 when "list"
83 when "list"
84 select_tag(field_name, options_for_select([[l(:label_no_change_option), '']] + custom_field.possible_values), :id => field_id)
84 select_tag(field_name, options_for_select([[l(:label_no_change_option), '']] + custom_field.possible_values), :id => field_id)
85 else
85 else
86 text_field_tag(field_name, '', :id => field_id)
86 text_field_tag(field_name, '', :id => field_id)
87 end
87 end
88 end
88 end
89
89
90 # Return a string used to display a custom value
90 # Return a string used to display a custom value
91 def show_value(custom_value)
91 def show_value(custom_value)
92 return "" unless custom_value
92 return "" unless custom_value
93 format_value(custom_value.value, custom_value.custom_field.field_format)
93 format_value(custom_value.value, custom_value.custom_field.field_format)
94 end
94 end
95
95
96 # Return a string used to display a custom value
96 # Return a string used to display a custom value
97 def format_value(value, field_format)
97 def format_value(value, field_format)
98 return "" unless value && !value.empty?
98 return "" unless value && !value.empty?
99 case field_format
99 case field_format
100 when "date"
100 when "date"
101 begin; format_date(value.to_date); rescue; value end
101 begin; format_date(value.to_date); rescue; value end
102 when "bool"
102 when "bool"
103 l(value == "1" ? :general_text_Yes : :general_text_No)
103 l(value == "1" ? :general_text_Yes : :general_text_No)
104 else
104 else
105 value
105 value
106 end
106 end
107 end
107 end
108
108
109 # Return an array of custom field formats which can be used in select_tag
109 # Return an array of custom field formats which can be used in select_tag
110 def custom_field_formats_for_select
110 def custom_field_formats_for_select
111 CustomField::FIELD_FORMATS.sort {|a,b| a[1][:order]<=>b[1][:order]}.collect { |k| [ l(k[1][:name]), k[0] ] }
111 CustomField::FIELD_FORMATS.sort {|a,b| a[1][:order]<=>b[1][:order]}.collect { |k| [ l(k[1][:name]), k[0] ] }
112 end
112 end
113 end
113 end
@@ -1,569 +1,557
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_attachable :after_remove => :attachment_removed
35 acts_as_attachable :after_remove => :attachment_removed
36 acts_as_customizable
36 acts_as_customizable
37 acts_as_watchable
37 acts_as_watchable
38 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
38 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
39 :include => [:project, :journals],
39 :include => [:project, :journals],
40 # sort by id so that limited eager loading doesn't break with postgresql
40 # sort by id so that limited eager loading doesn't break with postgresql
41 :order_column => "#{table_name}.id"
41 :order_column => "#{table_name}.id"
42 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
42 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
43 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
43 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
44 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
44 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
45
45
46 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
46 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
47 :author_key => :author_id
47 :author_key => :author_id
48
48
49 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
49 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
50
50
51 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
51 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
52 validates_length_of :subject, :maximum => 255
52 validates_length_of :subject, :maximum => 255
53 validates_inclusion_of :done_ratio, :in => 0..100
53 validates_inclusion_of :done_ratio, :in => 0..100
54 validates_numericality_of :estimated_hours, :allow_nil => true
54 validates_numericality_of :estimated_hours, :allow_nil => true
55
55
56 named_scope :visible, lambda {|*args| { :include => :project,
56 named_scope :visible, lambda {|*args| { :include => :project,
57 :conditions => Project.allowed_to_condition(args.first || User.current, :view_issues) } }
57 :conditions => Project.allowed_to_condition(args.first || User.current, :view_issues) } }
58
58
59 named_scope :open, :conditions => ["#{IssueStatus.table_name}.is_closed = ?", false], :include => :status
59 named_scope :open, :conditions => ["#{IssueStatus.table_name}.is_closed = ?", false], :include => :status
60
60
61 before_save :update_done_ratio_from_issue_status
61 before_save :update_done_ratio_from_issue_status
62 after_save :create_journal
62 after_save :create_journal
63
63
64 # Returns true if usr or current user is allowed to view the issue
64 # Returns true if usr or current user is allowed to view the issue
65 def visible?(usr=nil)
65 def visible?(usr=nil)
66 (usr || User.current).allowed_to?(:view_issues, self.project)
66 (usr || User.current).allowed_to?(:view_issues, self.project)
67 end
67 end
68
68
69 def after_initialize
69 def after_initialize
70 if new_record?
70 if new_record?
71 # set default values for new records only
71 # set default values for new records only
72 self.status ||= IssueStatus.default
72 self.status ||= IssueStatus.default
73 self.priority ||= IssuePriority.default
73 self.priority ||= IssuePriority.default
74 end
74 end
75 end
75 end
76
76
77 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
77 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
78 def available_custom_fields
78 def available_custom_fields
79 (project && tracker) ? project.all_issue_custom_fields.select {|c| tracker.custom_fields.include? c } : []
79 (project && tracker) ? project.all_issue_custom_fields.select {|c| tracker.custom_fields.include? c } : []
80 end
80 end
81
81
82 def copy_from(arg)
82 def copy_from(arg)
83 issue = arg.is_a?(Issue) ? arg : Issue.find(arg)
83 issue = arg.is_a?(Issue) ? arg : Issue.find(arg)
84 self.attributes = issue.attributes.dup.except("id", "created_on", "updated_on")
84 self.attributes = issue.attributes.dup.except("id", "created_on", "updated_on")
85 self.custom_values = issue.custom_values.collect {|v| v.clone}
85 self.custom_values = issue.custom_values.collect {|v| v.clone}
86 self.status = issue.status
86 self.status = issue.status
87 self
87 self
88 end
88 end
89
89
90 # Moves/copies an issue to a new project and tracker
90 # Moves/copies an issue to a new project and tracker
91 # Returns the moved/copied issue on success, false on failure
91 # Returns the moved/copied issue on success, false on failure
92 def move_to(new_project, new_tracker = nil, options = {})
92 def move_to(new_project, new_tracker = nil, options = {})
93 options ||= {}
93 options ||= {}
94 issue = options[:copy] ? self.clone : self
94 issue = options[:copy] ? self.clone : self
95 transaction do
95 transaction do
96 if new_project && issue.project_id != new_project.id
96 if new_project && issue.project_id != new_project.id
97 # delete issue relations
97 # delete issue relations
98 unless Setting.cross_project_issue_relations?
98 unless Setting.cross_project_issue_relations?
99 issue.relations_from.clear
99 issue.relations_from.clear
100 issue.relations_to.clear
100 issue.relations_to.clear
101 end
101 end
102 # issue is moved to another project
102 # issue is moved to another project
103 # reassign to the category with same name if any
103 # reassign to the category with same name if any
104 new_category = issue.category.nil? ? nil : new_project.issue_categories.find_by_name(issue.category.name)
104 new_category = issue.category.nil? ? nil : new_project.issue_categories.find_by_name(issue.category.name)
105 issue.category = new_category
105 issue.category = new_category
106 # Keep the fixed_version if it's still valid in the new_project
106 # Keep the fixed_version if it's still valid in the new_project
107 unless new_project.shared_versions.include?(issue.fixed_version)
107 unless new_project.shared_versions.include?(issue.fixed_version)
108 issue.fixed_version = nil
108 issue.fixed_version = nil
109 end
109 end
110 issue.project = new_project
110 issue.project = new_project
111 end
111 end
112 if new_tracker
112 if new_tracker
113 issue.tracker = new_tracker
113 issue.tracker = new_tracker
114 end
114 end
115 if options[:copy]
115 if options[:copy]
116 issue.custom_field_values = self.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
116 issue.custom_field_values = self.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
117 issue.status = if options[:attributes] && options[:attributes][:status_id]
117 issue.status = if options[:attributes] && options[:attributes][:status_id]
118 IssueStatus.find_by_id(options[:attributes][:status_id])
118 IssueStatus.find_by_id(options[:attributes][:status_id])
119 else
119 else
120 self.status
120 self.status
121 end
121 end
122 end
122 end
123 # Allow bulk setting of attributes on the issue
123 # Allow bulk setting of attributes on the issue
124 if options[:attributes]
124 if options[:attributes]
125 issue.attributes = options[:attributes]
125 issue.attributes = options[:attributes]
126 end
126 end
127 if issue.save
127 if issue.save
128 unless options[:copy]
128 unless options[:copy]
129 # Manually update project_id on related time entries
129 # Manually update project_id on related time entries
130 TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
130 TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
131 end
131 end
132 else
132 else
133 Issue.connection.rollback_db_transaction
133 Issue.connection.rollback_db_transaction
134 return false
134 return false
135 end
135 end
136 end
136 end
137 return issue
137 return issue
138 end
138 end
139
140 def bulk_edit(params)
141 journal = init_journal(User.current, params[:notes])
142 self.tracker = params[:tracker] if params[:tracker]
143 self.priority = params[:priority] if params[:priority]
144 self.assigned_to = params[:assigned_to] if params[:assigned_to] || params[:assigned_to_id] == 'none'
145 self.category = params[:category] if params[:category] || params[:category_id] == 'none'
146 self.fixed_version = params[:fixed_version] if params[:fixed_version] || params[:fixed_version_id] == 'none'
147 self.start_date = params[:start_date] unless params[:start_date].blank?
148 self.due_date = params[:due_date] unless params[:due_date].blank?
149 self.done_ratio = params[:done_ratio] unless params[:done_ratio].blank?
150 self.custom_field_values = params[:custom_field_values] if params[:custom_field_values] && !params[:custom_field_values].empty?
151 # TODO: Edit hook name
152 Redmine::Hook.call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => self })
153
154 # Don't save any change to the issue if the user is not authorized to apply the requested status
155 return (params[:status].nil? || (new_statuses_allowed_to(User.current).include?(params[:status]) && self.status = params[:status])) && save
156 end
157
139
158 def priority_id=(pid)
140 def priority_id=(pid)
159 self.priority = nil
141 self.priority = nil
160 write_attribute(:priority_id, pid)
142 write_attribute(:priority_id, pid)
161 end
143 end
162
144
163 def tracker_id=(tid)
145 def tracker_id=(tid)
164 self.tracker = nil
146 self.tracker = nil
165 result = write_attribute(:tracker_id, tid)
147 result = write_attribute(:tracker_id, tid)
166 @custom_field_values = nil
148 @custom_field_values = nil
167 result
149 result
168 end
150 end
169
151
170 # Overrides attributes= so that tracker_id gets assigned first
152 # Overrides attributes= so that tracker_id gets assigned first
171 def attributes_with_tracker_first=(new_attributes, *args)
153 def attributes_with_tracker_first=(new_attributes, *args)
172 return if new_attributes.nil?
154 return if new_attributes.nil?
173 new_tracker_id = new_attributes['tracker_id'] || new_attributes[:tracker_id]
155 new_tracker_id = new_attributes['tracker_id'] || new_attributes[:tracker_id]
174 if new_tracker_id
156 if new_tracker_id
175 self.tracker_id = new_tracker_id
157 self.tracker_id = new_tracker_id
176 end
158 end
177 send :attributes_without_tracker_first=, new_attributes, *args
159 send :attributes_without_tracker_first=, new_attributes, *args
178 end
160 end
179 # Do not redefine alias chain on reload (see #4838)
161 # Do not redefine alias chain on reload (see #4838)
180 alias_method_chain(:attributes=, :tracker_first) unless method_defined?(:attributes_without_tracker_first=)
162 alias_method_chain(:attributes=, :tracker_first) unless method_defined?(:attributes_without_tracker_first=)
181
163
182 def estimated_hours=(h)
164 def estimated_hours=(h)
183 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
165 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
184 end
166 end
185
167
186 SAFE_ATTRIBUTES = %w(
168 SAFE_ATTRIBUTES = %w(
187 tracker_id
169 tracker_id
188 status_id
170 status_id
189 category_id
171 category_id
190 assigned_to_id
172 assigned_to_id
191 priority_id
173 priority_id
192 fixed_version_id
174 fixed_version_id
193 subject
175 subject
194 description
176 description
195 start_date
177 start_date
196 due_date
178 due_date
197 done_ratio
179 done_ratio
198 estimated_hours
180 estimated_hours
199 custom_field_values
181 custom_field_values
200 ) unless const_defined?(:SAFE_ATTRIBUTES)
182 ) unless const_defined?(:SAFE_ATTRIBUTES)
201
183
202 # Safely sets attributes
184 # Safely sets attributes
203 # Should be called from controllers instead of #attributes=
185 # Should be called from controllers instead of #attributes=
204 # attr_accessible is too rough because we still want things like
186 # attr_accessible is too rough because we still want things like
205 # Issue.new(:project => foo) to work
187 # Issue.new(:project => foo) to work
206 # TODO: move workflow/permission checks from controllers to here
188 # TODO: move workflow/permission checks from controllers to here
207 def safe_attributes=(attrs, user=User.current)
189 def safe_attributes=(attrs, user=User.current)
208 return if attrs.nil?
190 return if attrs.nil?
209 self.attributes = attrs.reject {|k,v| !SAFE_ATTRIBUTES.include?(k)}
191 attrs = attrs.reject {|k,v| !SAFE_ATTRIBUTES.include?(k)}
192 if attrs['status_id']
193 unless new_statuses_allowed_to(user).collect(&:id).include?(attrs['status_id'].to_i)
194 attrs.delete('status_id')
195 end
196 end
197 self.attributes = attrs
210 end
198 end
211
199
212 def done_ratio
200 def done_ratio
213 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio?
201 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio?
214 status.default_done_ratio
202 status.default_done_ratio
215 else
203 else
216 read_attribute(:done_ratio)
204 read_attribute(:done_ratio)
217 end
205 end
218 end
206 end
219
207
220 def self.use_status_for_done_ratio?
208 def self.use_status_for_done_ratio?
221 Setting.issue_done_ratio == 'issue_status'
209 Setting.issue_done_ratio == 'issue_status'
222 end
210 end
223
211
224 def self.use_field_for_done_ratio?
212 def self.use_field_for_done_ratio?
225 Setting.issue_done_ratio == 'issue_field'
213 Setting.issue_done_ratio == 'issue_field'
226 end
214 end
227
215
228 def validate
216 def validate
229 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
217 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
230 errors.add :due_date, :not_a_date
218 errors.add :due_date, :not_a_date
231 end
219 end
232
220
233 if self.due_date and self.start_date and self.due_date < self.start_date
221 if self.due_date and self.start_date and self.due_date < self.start_date
234 errors.add :due_date, :greater_than_start_date
222 errors.add :due_date, :greater_than_start_date
235 end
223 end
236
224
237 if start_date && soonest_start && start_date < soonest_start
225 if start_date && soonest_start && start_date < soonest_start
238 errors.add :start_date, :invalid
226 errors.add :start_date, :invalid
239 end
227 end
240
228
241 if fixed_version
229 if fixed_version
242 if !assignable_versions.include?(fixed_version)
230 if !assignable_versions.include?(fixed_version)
243 errors.add :fixed_version_id, :inclusion
231 errors.add :fixed_version_id, :inclusion
244 elsif reopened? && fixed_version.closed?
232 elsif reopened? && fixed_version.closed?
245 errors.add_to_base I18n.t(:error_can_not_reopen_issue_on_closed_version)
233 errors.add_to_base I18n.t(:error_can_not_reopen_issue_on_closed_version)
246 end
234 end
247 end
235 end
248
236
249 # Checks that the issue can not be added/moved to a disabled tracker
237 # Checks that the issue can not be added/moved to a disabled tracker
250 if project && (tracker_id_changed? || project_id_changed?)
238 if project && (tracker_id_changed? || project_id_changed?)
251 unless project.trackers.include?(tracker)
239 unless project.trackers.include?(tracker)
252 errors.add :tracker_id, :inclusion
240 errors.add :tracker_id, :inclusion
253 end
241 end
254 end
242 end
255 end
243 end
256
244
257 def before_create
245 def before_create
258 # default assignment based on category
246 # default assignment based on category
259 if assigned_to.nil? && category && category.assigned_to
247 if assigned_to.nil? && category && category.assigned_to
260 self.assigned_to = category.assigned_to
248 self.assigned_to = category.assigned_to
261 end
249 end
262 end
250 end
263
251
264 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
252 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
265 # even if the user turns off the setting later
253 # even if the user turns off the setting later
266 def update_done_ratio_from_issue_status
254 def update_done_ratio_from_issue_status
267 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio?
255 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio?
268 self.done_ratio = status.default_done_ratio
256 self.done_ratio = status.default_done_ratio
269 end
257 end
270 end
258 end
271
259
272 def after_save
260 def after_save
273 # Reload is needed in order to get the right status
261 # Reload is needed in order to get the right status
274 reload
262 reload
275
263
276 # Update start/due dates of following issues
264 # Update start/due dates of following issues
277 relations_from.each(&:set_issue_to_dates)
265 relations_from.each(&:set_issue_to_dates)
278
266
279 # Close duplicates if the issue was closed
267 # Close duplicates if the issue was closed
280 if @issue_before_change && !@issue_before_change.closed? && self.closed?
268 if @issue_before_change && !@issue_before_change.closed? && self.closed?
281 duplicates.each do |duplicate|
269 duplicates.each do |duplicate|
282 # Reload is need in case the duplicate was updated by a previous duplicate
270 # Reload is need in case the duplicate was updated by a previous duplicate
283 duplicate.reload
271 duplicate.reload
284 # Don't re-close it if it's already closed
272 # Don't re-close it if it's already closed
285 next if duplicate.closed?
273 next if duplicate.closed?
286 # Same user and notes
274 # Same user and notes
287 duplicate.init_journal(@current_journal.user, @current_journal.notes)
275 duplicate.init_journal(@current_journal.user, @current_journal.notes)
288 duplicate.update_attribute :status, self.status
276 duplicate.update_attribute :status, self.status
289 end
277 end
290 end
278 end
291 end
279 end
292
280
293 def init_journal(user, notes = "")
281 def init_journal(user, notes = "")
294 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
282 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
295 @issue_before_change = self.clone
283 @issue_before_change = self.clone
296 @issue_before_change.status = self.status
284 @issue_before_change.status = self.status
297 @custom_values_before_change = {}
285 @custom_values_before_change = {}
298 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
286 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
299 # Make sure updated_on is updated when adding a note.
287 # Make sure updated_on is updated when adding a note.
300 updated_on_will_change!
288 updated_on_will_change!
301 @current_journal
289 @current_journal
302 end
290 end
303
291
304 # Return true if the issue is closed, otherwise false
292 # Return true if the issue is closed, otherwise false
305 def closed?
293 def closed?
306 self.status.is_closed?
294 self.status.is_closed?
307 end
295 end
308
296
309 # Return true if the issue is being reopened
297 # Return true if the issue is being reopened
310 def reopened?
298 def reopened?
311 if !new_record? && status_id_changed?
299 if !new_record? && status_id_changed?
312 status_was = IssueStatus.find_by_id(status_id_was)
300 status_was = IssueStatus.find_by_id(status_id_was)
313 status_new = IssueStatus.find_by_id(status_id)
301 status_new = IssueStatus.find_by_id(status_id)
314 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
302 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
315 return true
303 return true
316 end
304 end
317 end
305 end
318 false
306 false
319 end
307 end
320
308
321 # Returns true if the issue is overdue
309 # Returns true if the issue is overdue
322 def overdue?
310 def overdue?
323 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
311 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
324 end
312 end
325
313
326 # Users the issue can be assigned to
314 # Users the issue can be assigned to
327 def assignable_users
315 def assignable_users
328 project.assignable_users
316 project.assignable_users
329 end
317 end
330
318
331 # Versions that the issue can be assigned to
319 # Versions that the issue can be assigned to
332 def assignable_versions
320 def assignable_versions
333 @assignable_versions ||= (project.shared_versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort
321 @assignable_versions ||= (project.shared_versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort
334 end
322 end
335
323
336 # Returns true if this issue is blocked by another issue that is still open
324 # Returns true if this issue is blocked by another issue that is still open
337 def blocked?
325 def blocked?
338 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
326 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
339 end
327 end
340
328
341 # Returns an array of status that user is able to apply
329 # Returns an array of status that user is able to apply
342 def new_statuses_allowed_to(user)
330 def new_statuses_allowed_to(user)
343 statuses = status.find_new_statuses_allowed_to(user.roles_for_project(project), tracker)
331 statuses = status.find_new_statuses_allowed_to(user.roles_for_project(project), tracker)
344 statuses << status unless statuses.empty?
332 statuses << status unless statuses.empty?
345 statuses = statuses.uniq.sort
333 statuses = statuses.uniq.sort
346 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
334 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
347 end
335 end
348
336
349 # Returns the mail adresses of users that should be notified
337 # Returns the mail adresses of users that should be notified
350 def recipients
338 def recipients
351 notified = project.notified_users
339 notified = project.notified_users
352 # Author and assignee are always notified unless they have been locked
340 # Author and assignee are always notified unless they have been locked
353 notified << author if author && author.active?
341 notified << author if author && author.active?
354 notified << assigned_to if assigned_to && assigned_to.active?
342 notified << assigned_to if assigned_to && assigned_to.active?
355 notified.uniq!
343 notified.uniq!
356 # Remove users that can not view the issue
344 # Remove users that can not view the issue
357 notified.reject! {|user| !visible?(user)}
345 notified.reject! {|user| !visible?(user)}
358 notified.collect(&:mail)
346 notified.collect(&:mail)
359 end
347 end
360
348
361 # Returns the total number of hours spent on this issue.
349 # Returns the total number of hours spent on this issue.
362 #
350 #
363 # Example:
351 # Example:
364 # spent_hours => 0
352 # spent_hours => 0
365 # spent_hours => 50
353 # spent_hours => 50
366 def spent_hours
354 def spent_hours
367 @spent_hours ||= time_entries.sum(:hours) || 0
355 @spent_hours ||= time_entries.sum(:hours) || 0
368 end
356 end
369
357
370 def relations
358 def relations
371 (relations_from + relations_to).sort
359 (relations_from + relations_to).sort
372 end
360 end
373
361
374 def all_dependent_issues
362 def all_dependent_issues
375 dependencies = []
363 dependencies = []
376 relations_from.each do |relation|
364 relations_from.each do |relation|
377 dependencies << relation.issue_to
365 dependencies << relation.issue_to
378 dependencies += relation.issue_to.all_dependent_issues
366 dependencies += relation.issue_to.all_dependent_issues
379 end
367 end
380 dependencies
368 dependencies
381 end
369 end
382
370
383 # Returns an array of issues that duplicate this one
371 # Returns an array of issues that duplicate this one
384 def duplicates
372 def duplicates
385 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
373 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
386 end
374 end
387
375
388 # Returns the due date or the target due date if any
376 # Returns the due date or the target due date if any
389 # Used on gantt chart
377 # Used on gantt chart
390 def due_before
378 def due_before
391 due_date || (fixed_version ? fixed_version.effective_date : nil)
379 due_date || (fixed_version ? fixed_version.effective_date : nil)
392 end
380 end
393
381
394 # Returns the time scheduled for this issue.
382 # Returns the time scheduled for this issue.
395 #
383 #
396 # Example:
384 # Example:
397 # Start Date: 2/26/09, End Date: 3/04/09
385 # Start Date: 2/26/09, End Date: 3/04/09
398 # duration => 6
386 # duration => 6
399 def duration
387 def duration
400 (start_date && due_date) ? due_date - start_date : 0
388 (start_date && due_date) ? due_date - start_date : 0
401 end
389 end
402
390
403 def soonest_start
391 def soonest_start
404 @soonest_start ||= relations_to.collect{|relation| relation.successor_soonest_start}.compact.min
392 @soonest_start ||= relations_to.collect{|relation| relation.successor_soonest_start}.compact.min
405 end
393 end
406
394
407 def to_s
395 def to_s
408 "#{tracker} ##{id}: #{subject}"
396 "#{tracker} ##{id}: #{subject}"
409 end
397 end
410
398
411 # Returns a string of css classes that apply to the issue
399 # Returns a string of css classes that apply to the issue
412 def css_classes
400 def css_classes
413 s = "issue status-#{status.position} priority-#{priority.position}"
401 s = "issue status-#{status.position} priority-#{priority.position}"
414 s << ' closed' if closed?
402 s << ' closed' if closed?
415 s << ' overdue' if overdue?
403 s << ' overdue' if overdue?
416 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
404 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
417 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
405 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
418 s
406 s
419 end
407 end
420
408
421 # Unassigns issues from +version+ if it's no longer shared with issue's project
409 # Unassigns issues from +version+ if it's no longer shared with issue's project
422 def self.update_versions_from_sharing_change(version)
410 def self.update_versions_from_sharing_change(version)
423 # Update issues assigned to the version
411 # Update issues assigned to the version
424 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
412 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
425 end
413 end
426
414
427 # Unassigns issues from versions that are no longer shared
415 # Unassigns issues from versions that are no longer shared
428 # after +project+ was moved
416 # after +project+ was moved
429 def self.update_versions_from_hierarchy_change(project)
417 def self.update_versions_from_hierarchy_change(project)
430 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
418 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
431 # Update issues of the moved projects and issues assigned to a version of a moved project
419 # Update issues of the moved projects and issues assigned to a version of a moved project
432 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
420 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
433 end
421 end
434
422
435 # Extracted from the ReportsController.
423 # Extracted from the ReportsController.
436 def self.by_tracker(project)
424 def self.by_tracker(project)
437 count_and_group_by(:project => project,
425 count_and_group_by(:project => project,
438 :field => 'tracker_id',
426 :field => 'tracker_id',
439 :joins => Tracker.table_name)
427 :joins => Tracker.table_name)
440 end
428 end
441
429
442 def self.by_version(project)
430 def self.by_version(project)
443 count_and_group_by(:project => project,
431 count_and_group_by(:project => project,
444 :field => 'fixed_version_id',
432 :field => 'fixed_version_id',
445 :joins => Version.table_name)
433 :joins => Version.table_name)
446 end
434 end
447
435
448 def self.by_priority(project)
436 def self.by_priority(project)
449 count_and_group_by(:project => project,
437 count_and_group_by(:project => project,
450 :field => 'priority_id',
438 :field => 'priority_id',
451 :joins => IssuePriority.table_name)
439 :joins => IssuePriority.table_name)
452 end
440 end
453
441
454 def self.by_category(project)
442 def self.by_category(project)
455 count_and_group_by(:project => project,
443 count_and_group_by(:project => project,
456 :field => 'category_id',
444 :field => 'category_id',
457 :joins => IssueCategory.table_name)
445 :joins => IssueCategory.table_name)
458 end
446 end
459
447
460 def self.by_assigned_to(project)
448 def self.by_assigned_to(project)
461 count_and_group_by(:project => project,
449 count_and_group_by(:project => project,
462 :field => 'assigned_to_id',
450 :field => 'assigned_to_id',
463 :joins => User.table_name)
451 :joins => User.table_name)
464 end
452 end
465
453
466 def self.by_author(project)
454 def self.by_author(project)
467 count_and_group_by(:project => project,
455 count_and_group_by(:project => project,
468 :field => 'author_id',
456 :field => 'author_id',
469 :joins => User.table_name)
457 :joins => User.table_name)
470 end
458 end
471
459
472 def self.by_subproject(project)
460 def self.by_subproject(project)
473 ActiveRecord::Base.connection.select_all("select s.id as status_id,
461 ActiveRecord::Base.connection.select_all("select s.id as status_id,
474 s.is_closed as closed,
462 s.is_closed as closed,
475 i.project_id as project_id,
463 i.project_id as project_id,
476 count(i.id) as total
464 count(i.id) as total
477 from
465 from
478 #{Issue.table_name} i, #{IssueStatus.table_name} s
466 #{Issue.table_name} i, #{IssueStatus.table_name} s
479 where
467 where
480 i.status_id=s.id
468 i.status_id=s.id
481 and i.project_id IN (#{project.descendants.active.collect{|p| p.id}.join(',')})
469 and i.project_id IN (#{project.descendants.active.collect{|p| p.id}.join(',')})
482 group by s.id, s.is_closed, i.project_id") if project.descendants.active.any?
470 group by s.id, s.is_closed, i.project_id") if project.descendants.active.any?
483 end
471 end
484 # End ReportsController extraction
472 # End ReportsController extraction
485
473
486 private
474 private
487
475
488 # Update issues so their versions are not pointing to a
476 # Update issues so their versions are not pointing to a
489 # fixed_version that is not shared with the issue's project
477 # fixed_version that is not shared with the issue's project
490 def self.update_versions(conditions=nil)
478 def self.update_versions(conditions=nil)
491 # Only need to update issues with a fixed_version from
479 # Only need to update issues with a fixed_version from
492 # a different project and that is not systemwide shared
480 # a different project and that is not systemwide shared
493 Issue.all(:conditions => merge_conditions("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
481 Issue.all(:conditions => merge_conditions("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
494 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
482 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
495 " AND #{Version.table_name}.sharing <> 'system'",
483 " AND #{Version.table_name}.sharing <> 'system'",
496 conditions),
484 conditions),
497 :include => [:project, :fixed_version]
485 :include => [:project, :fixed_version]
498 ).each do |issue|
486 ).each do |issue|
499 next if issue.project.nil? || issue.fixed_version.nil?
487 next if issue.project.nil? || issue.fixed_version.nil?
500 unless issue.project.shared_versions.include?(issue.fixed_version)
488 unless issue.project.shared_versions.include?(issue.fixed_version)
501 issue.init_journal(User.current)
489 issue.init_journal(User.current)
502 issue.fixed_version = nil
490 issue.fixed_version = nil
503 issue.save
491 issue.save
504 end
492 end
505 end
493 end
506 end
494 end
507
495
508 # Callback on attachment deletion
496 # Callback on attachment deletion
509 def attachment_removed(obj)
497 def attachment_removed(obj)
510 journal = init_journal(User.current)
498 journal = init_journal(User.current)
511 journal.details << JournalDetail.new(:property => 'attachment',
499 journal.details << JournalDetail.new(:property => 'attachment',
512 :prop_key => obj.id,
500 :prop_key => obj.id,
513 :old_value => obj.filename)
501 :old_value => obj.filename)
514 journal.save
502 journal.save
515 end
503 end
516
504
517 # Saves the changes in a Journal
505 # Saves the changes in a Journal
518 # Called after_save
506 # Called after_save
519 def create_journal
507 def create_journal
520 if @current_journal
508 if @current_journal
521 # attributes changes
509 # attributes changes
522 (Issue.column_names - %w(id description lock_version created_on updated_on)).each {|c|
510 (Issue.column_names - %w(id description lock_version created_on updated_on)).each {|c|
523 @current_journal.details << JournalDetail.new(:property => 'attr',
511 @current_journal.details << JournalDetail.new(:property => 'attr',
524 :prop_key => c,
512 :prop_key => c,
525 :old_value => @issue_before_change.send(c),
513 :old_value => @issue_before_change.send(c),
526 :value => send(c)) unless send(c)==@issue_before_change.send(c)
514 :value => send(c)) unless send(c)==@issue_before_change.send(c)
527 }
515 }
528 # custom fields changes
516 # custom fields changes
529 custom_values.each {|c|
517 custom_values.each {|c|
530 next if (@custom_values_before_change[c.custom_field_id]==c.value ||
518 next if (@custom_values_before_change[c.custom_field_id]==c.value ||
531 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
519 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
532 @current_journal.details << JournalDetail.new(:property => 'cf',
520 @current_journal.details << JournalDetail.new(:property => 'cf',
533 :prop_key => c.custom_field_id,
521 :prop_key => c.custom_field_id,
534 :old_value => @custom_values_before_change[c.custom_field_id],
522 :old_value => @custom_values_before_change[c.custom_field_id],
535 :value => c.value)
523 :value => c.value)
536 }
524 }
537 @current_journal.save
525 @current_journal.save
538 end
526 end
539 end
527 end
540
528
541 # Query generator for selecting groups of issue counts for a project
529 # Query generator for selecting groups of issue counts for a project
542 # based on specific criteria
530 # based on specific criteria
543 #
531 #
544 # Options
532 # Options
545 # * project - Project to search in.
533 # * project - Project to search in.
546 # * field - String. Issue field to key off of in the grouping.
534 # * field - String. Issue field to key off of in the grouping.
547 # * joins - String. The table name to join against.
535 # * joins - String. The table name to join against.
548 def self.count_and_group_by(options)
536 def self.count_and_group_by(options)
549 project = options.delete(:project)
537 project = options.delete(:project)
550 select_field = options.delete(:field)
538 select_field = options.delete(:field)
551 joins = options.delete(:joins)
539 joins = options.delete(:joins)
552
540
553 where = "i.#{select_field}=j.id"
541 where = "i.#{select_field}=j.id"
554
542
555 ActiveRecord::Base.connection.select_all("select s.id as status_id,
543 ActiveRecord::Base.connection.select_all("select s.id as status_id,
556 s.is_closed as closed,
544 s.is_closed as closed,
557 j.id as #{select_field},
545 j.id as #{select_field},
558 count(i.id) as total
546 count(i.id) as total
559 from
547 from
560 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{joins} as j
548 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{joins} as j
561 where
549 where
562 i.status_id=s.id
550 i.status_id=s.id
563 and #{where}
551 and #{where}
564 and i.project_id=#{project.id}
552 and i.project_id=#{project.id}
565 group by s.id, s.is_closed, j.id")
553 group by s.id, s.is_closed, j.id")
566 end
554 end
567
555
568
556
569 end
557 end
@@ -1,78 +1,78
1 <h2><%= l(:label_bulk_edit_selected_issues) %></h2>
1 <h2><%= l(:label_bulk_edit_selected_issues) %></h2>
2
2
3 <ul><%= @issues.collect {|i| content_tag('li', link_to(h("#{i.tracker} ##{i.id}"), { :action => 'show', :id => i }) + h(": #{i.subject}")) }.join("\n") %></ul>
3 <ul><%= @issues.collect {|i| content_tag('li', link_to(h("#{i.tracker} ##{i.id}"), { :action => 'show', :id => i }) + h(": #{i.subject}")) }.join("\n") %></ul>
4
4
5 <% form_tag() do %>
5 <% form_tag() do %>
6 <%= @issues.collect {|i| hidden_field_tag('ids[]', i.id)}.join %>
6 <%= @issues.collect {|i| hidden_field_tag('ids[]', i.id)}.join %>
7 <div class="box tabular">
7 <div class="box tabular">
8 <fieldset>
8 <fieldset>
9 <legend><%= l(:label_change_properties) %></legend>
9 <legend><%= l(:label_change_properties) %></legend>
10
10
11 <div class="splitcontentleft">
11 <div class="splitcontentleft">
12 <p>
12 <p>
13 <label><%= l(:field_tracker) %></label>
13 <label><%= l(:field_tracker) %></label>
14 <%= select_tag('tracker_id', "<option value=\"\">#{l(:label_no_change_option)}</option>" + options_from_collection_for_select(@project.trackers, :id, :name)) %>
14 <%= select_tag('issue[tracker_id]', "<option value=\"\">#{l(:label_no_change_option)}</option>" + options_from_collection_for_select(@project.trackers, :id, :name)) %>
15 </p>
15 </p>
16 <% if @available_statuses.any? %>
16 <% if @available_statuses.any? %>
17 <p>
17 <p>
18 <label><%= l(:field_status) %></label>
18 <label><%= l(:field_status) %></label>
19 <%= select_tag('status_id', "<option value=\"\">#{l(:label_no_change_option)}</option>" + options_from_collection_for_select(@available_statuses, :id, :name)) %>
19 <%= select_tag('issue[status_id]', "<option value=\"\">#{l(:label_no_change_option)}</option>" + options_from_collection_for_select(@available_statuses, :id, :name)) %>
20 </p>
20 </p>
21 <% end %>
21 <% end %>
22 <p>
22 <p>
23 <label><%= l(:field_priority) %></label>
23 <label><%= l(:field_priority) %></label>
24 <%= select_tag('priority_id', "<option value=\"\">#{l(:label_no_change_option)}</option>" + options_from_collection_for_select(IssuePriority.all, :id, :name)) %>
24 <%= select_tag('issue[priority_id]', "<option value=\"\">#{l(:label_no_change_option)}</option>" + options_from_collection_for_select(IssuePriority.all, :id, :name)) %>
25 </p>
25 </p>
26 <p>
26 <p>
27 <label><%= l(:field_assigned_to) %></label>
27 <label><%= l(:field_assigned_to) %></label>
28 <%= select_tag('assigned_to_id', content_tag('option', l(:label_no_change_option), :value => '') +
28 <%= select_tag('issue[assigned_to_id]', content_tag('option', l(:label_no_change_option), :value => '') +
29 content_tag('option', l(:label_nobody), :value => 'none') +
29 content_tag('option', l(:label_nobody), :value => 'none') +
30 options_from_collection_for_select(@project.assignable_users, :id, :name)) %>
30 options_from_collection_for_select(@project.assignable_users, :id, :name)) %>
31 </p>
31 </p>
32 <p>
32 <p>
33 <label><%= l(:field_category) %></label>
33 <label><%= l(:field_category) %></label>
34 <%= select_tag('category_id', content_tag('option', l(:label_no_change_option), :value => '') +
34 <%= select_tag('issue[category_id]', content_tag('option', l(:label_no_change_option), :value => '') +
35 content_tag('option', l(:label_none), :value => 'none') +
35 content_tag('option', l(:label_none), :value => 'none') +
36 options_from_collection_for_select(@project.issue_categories, :id, :name)) %>
36 options_from_collection_for_select(@project.issue_categories, :id, :name)) %>
37 </p>
37 </p>
38 <p>
38 <p>
39 <label><%= l(:field_fixed_version) %></label>
39 <label><%= l(:field_fixed_version) %></label>
40 <%= select_tag('fixed_version_id', content_tag('option', l(:label_no_change_option), :value => '') +
40 <%= select_tag('issue[fixed_version_id]', content_tag('option', l(:label_no_change_option), :value => '') +
41 content_tag('option', l(:label_none), :value => 'none') +
41 content_tag('option', l(:label_none), :value => 'none') +
42 version_options_for_select(@project.shared_versions.open)) %>
42 version_options_for_select(@project.shared_versions.open)) %>
43 </p>
43 </p>
44
44
45 <% @custom_fields.each do |custom_field| %>
45 <% @custom_fields.each do |custom_field| %>
46 <p><label><%= h(custom_field.name) %></label> <%= custom_field_tag_for_bulk_edit(custom_field) %></p>
46 <p><label><%= h(custom_field.name) %></label> <%= custom_field_tag_for_bulk_edit('issue', custom_field) %></p>
47 <% end %>
47 <% end %>
48
48
49 <%= call_hook(:view_issues_bulk_edit_details_bottom, { :issues => @issues }) %>
49 <%= call_hook(:view_issues_bulk_edit_details_bottom, { :issues => @issues }) %>
50 </div>
50 </div>
51
51
52 <div class="splitcontentright">
52 <div class="splitcontentright">
53 <p>
53 <p>
54 <label><%= l(:field_start_date) %></label>
54 <label><%= l(:field_start_date) %></label>
55 <%= text_field_tag 'start_date', '', :size => 10 %><%= calendar_for('start_date') %>
55 <%= text_field_tag 'issue[start_date]', '', :size => 10 %><%= calendar_for('issue_start_date') %>
56 </p>
56 </p>
57 <p>
57 <p>
58 <label><%= l(:field_due_date) %></label>
58 <label><%= l(:field_due_date) %></label>
59 <%= text_field_tag 'due_date', '', :size => 10 %><%= calendar_for('due_date') %>
59 <%= text_field_tag 'issue[due_date]', '', :size => 10 %><%= calendar_for('issue_due_date') %>
60 </p>
60 </p>
61 <% if Issue.use_field_for_done_ratio? %>
61 <% if Issue.use_field_for_done_ratio? %>
62 <p>
62 <p>
63 <label><%= l(:field_done_ratio) %></label>
63 <label><%= l(:field_done_ratio) %></label>
64 <%= select_tag 'done_ratio', options_for_select([[l(:label_no_change_option), '']] + (0..10).to_a.collect {|r| ["#{r*10} %", r*10] }) %>
64 <%= select_tag 'issue[done_ratio]', options_for_select([[l(:label_no_change_option), '']] + (0..10).to_a.collect {|r| ["#{r*10} %", r*10] }) %>
65 </p>
65 </p>
66 <% end %>
66 <% end %>
67 </div>
67 </div>
68
68
69 </fieldset>
69 </fieldset>
70
70
71 <fieldset><legend><%= l(:field_notes) %></legend>
71 <fieldset><legend><%= l(:field_notes) %></legend>
72 <%= text_area_tag 'notes', @notes, :cols => 60, :rows => 10, :class => 'wiki-edit' %>
72 <%= text_area_tag 'notes', @notes, :cols => 60, :rows => 10, :class => 'wiki-edit' %>
73 <%= wikitoolbar_for 'notes' %>
73 <%= wikitoolbar_for 'notes' %>
74 </fieldset>
74 </fieldset>
75 </div>
75 </div>
76
76
77 <p><%= submit_tag l(:button_submit) %></p>
77 <p><%= submit_tag l(:button_submit) %></p>
78 <% end %>
78 <% end %>
@@ -1,1230 +1,1233
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_filter
128 def test_index_with_project_and_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 end
133 end
134
134
135 def test_index_with_query
135 def test_index_with_query
136 get :index, :project_id => 1, :query_id => 5
136 get :index, :project_id => 1, :query_id => 5
137 assert_response :success
137 assert_response :success
138 assert_template 'index.rhtml'
138 assert_template 'index.rhtml'
139 assert_not_nil assigns(:issues)
139 assert_not_nil assigns(:issues)
140 assert_nil assigns(:issue_count_by_group)
140 assert_nil assigns(:issue_count_by_group)
141 end
141 end
142
142
143 def test_index_with_query_grouped_by_tracker
143 def test_index_with_query_grouped_by_tracker
144 get :index, :project_id => 1, :query_id => 6
144 get :index, :project_id => 1, :query_id => 6
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 assert_not_nil assigns(:issue_count_by_group)
148 assert_not_nil assigns(:issue_count_by_group)
149 end
149 end
150
150
151 def test_index_with_query_grouped_by_list_custom_field
151 def test_index_with_query_grouped_by_list_custom_field
152 get :index, :project_id => 1, :query_id => 9
152 get :index, :project_id => 1, :query_id => 9
153 assert_response :success
153 assert_response :success
154 assert_template 'index.rhtml'
154 assert_template 'index.rhtml'
155 assert_not_nil assigns(:issues)
155 assert_not_nil assigns(:issues)
156 assert_not_nil assigns(:issue_count_by_group)
156 assert_not_nil assigns(:issue_count_by_group)
157 end
157 end
158
158
159 def test_index_sort_by_field_not_included_in_columns
159 def test_index_sort_by_field_not_included_in_columns
160 Setting.issue_list_default_columns = %w(subject author)
160 Setting.issue_list_default_columns = %w(subject author)
161 get :index, :sort => 'tracker'
161 get :index, :sort => 'tracker'
162 end
162 end
163
163
164 def test_index_csv_with_project
164 def test_index_csv_with_project
165 Setting.default_language = 'en'
165 Setting.default_language = 'en'
166
166
167 get :index, :format => 'csv'
167 get :index, :format => 'csv'
168 assert_response :success
168 assert_response :success
169 assert_not_nil assigns(:issues)
169 assert_not_nil assigns(:issues)
170 assert_equal 'text/csv', @response.content_type
170 assert_equal 'text/csv', @response.content_type
171 assert @response.body.starts_with?("#,")
171 assert @response.body.starts_with?("#,")
172
172
173 get :index, :project_id => 1, :format => 'csv'
173 get :index, :project_id => 1, :format => 'csv'
174 assert_response :success
174 assert_response :success
175 assert_not_nil assigns(:issues)
175 assert_not_nil assigns(:issues)
176 assert_equal 'text/csv', @response.content_type
176 assert_equal 'text/csv', @response.content_type
177 end
177 end
178
178
179 def test_index_pdf
179 def test_index_pdf
180 get :index, :format => 'pdf'
180 get :index, :format => 'pdf'
181 assert_response :success
181 assert_response :success
182 assert_not_nil assigns(:issues)
182 assert_not_nil assigns(:issues)
183 assert_equal 'application/pdf', @response.content_type
183 assert_equal 'application/pdf', @response.content_type
184
184
185 get :index, :project_id => 1, :format => 'pdf'
185 get :index, :project_id => 1, :format => 'pdf'
186 assert_response :success
186 assert_response :success
187 assert_not_nil assigns(:issues)
187 assert_not_nil assigns(:issues)
188 assert_equal 'application/pdf', @response.content_type
188 assert_equal 'application/pdf', @response.content_type
189
189
190 get :index, :project_id => 1, :query_id => 6, :format => 'pdf'
190 get :index, :project_id => 1, :query_id => 6, :format => 'pdf'
191 assert_response :success
191 assert_response :success
192 assert_not_nil assigns(:issues)
192 assert_not_nil assigns(:issues)
193 assert_equal 'application/pdf', @response.content_type
193 assert_equal 'application/pdf', @response.content_type
194 end
194 end
195
195
196 def test_index_pdf_with_query_grouped_by_list_custom_field
196 def test_index_pdf_with_query_grouped_by_list_custom_field
197 get :index, :project_id => 1, :query_id => 9, :format => 'pdf'
197 get :index, :project_id => 1, :query_id => 9, :format => 'pdf'
198 assert_response :success
198 assert_response :success
199 assert_not_nil assigns(:issues)
199 assert_not_nil assigns(:issues)
200 assert_not_nil assigns(:issue_count_by_group)
200 assert_not_nil assigns(:issue_count_by_group)
201 assert_equal 'application/pdf', @response.content_type
201 assert_equal 'application/pdf', @response.content_type
202 end
202 end
203
203
204 def test_index_sort
204 def test_index_sort
205 get :index, :sort => 'tracker,id:desc'
205 get :index, :sort => 'tracker,id:desc'
206 assert_response :success
206 assert_response :success
207
207
208 sort_params = @request.session['issues_index_sort']
208 sort_params = @request.session['issues_index_sort']
209 assert sort_params.is_a?(String)
209 assert sort_params.is_a?(String)
210 assert_equal 'tracker,id:desc', sort_params
210 assert_equal 'tracker,id:desc', sort_params
211
211
212 issues = assigns(:issues)
212 issues = assigns(:issues)
213 assert_not_nil issues
213 assert_not_nil issues
214 assert !issues.empty?
214 assert !issues.empty?
215 assert_equal issues.sort {|a,b| a.tracker == b.tracker ? b.id <=> a.id : a.tracker <=> b.tracker }.collect(&:id), issues.collect(&:id)
215 assert_equal issues.sort {|a,b| a.tracker == b.tracker ? b.id <=> a.id : a.tracker <=> b.tracker }.collect(&:id), issues.collect(&:id)
216 end
216 end
217
217
218 def test_index_with_columns
218 def test_index_with_columns
219 columns = ['tracker', 'subject', 'assigned_to']
219 columns = ['tracker', 'subject', 'assigned_to']
220 get :index, :set_filter => 1, :query => { 'column_names' => columns}
220 get :index, :set_filter => 1, :query => { 'column_names' => columns}
221 assert_response :success
221 assert_response :success
222
222
223 # query should use specified columns
223 # query should use specified columns
224 query = assigns(:query)
224 query = assigns(:query)
225 assert_kind_of Query, query
225 assert_kind_of Query, query
226 assert_equal columns, query.column_names.map(&:to_s)
226 assert_equal columns, query.column_names.map(&:to_s)
227
227
228 # columns should be stored in session
228 # columns should be stored in session
229 assert_kind_of Hash, session[:query]
229 assert_kind_of Hash, session[:query]
230 assert_kind_of Array, session[:query][:column_names]
230 assert_kind_of Array, session[:query][:column_names]
231 assert_equal columns, session[:query][:column_names].map(&:to_s)
231 assert_equal columns, session[:query][:column_names].map(&:to_s)
232 end
232 end
233
233
234 def test_gantt
234 def test_gantt
235 get :gantt, :project_id => 1
235 get :gantt, :project_id => 1
236 assert_response :success
236 assert_response :success
237 assert_template 'gantt.rhtml'
237 assert_template 'gantt.rhtml'
238 assert_not_nil assigns(:gantt)
238 assert_not_nil assigns(:gantt)
239 events = assigns(:gantt).events
239 events = assigns(:gantt).events
240 assert_not_nil events
240 assert_not_nil events
241 # Issue with start and due dates
241 # Issue with start and due dates
242 i = Issue.find(1)
242 i = Issue.find(1)
243 assert_not_nil i.due_date
243 assert_not_nil i.due_date
244 assert events.include?(Issue.find(1))
244 assert events.include?(Issue.find(1))
245 # Issue with without due date but targeted to a version with date
245 # Issue with without due date but targeted to a version with date
246 i = Issue.find(2)
246 i = Issue.find(2)
247 assert_nil i.due_date
247 assert_nil i.due_date
248 assert events.include?(i)
248 assert events.include?(i)
249 end
249 end
250
250
251 def test_cross_project_gantt
251 def test_cross_project_gantt
252 get :gantt
252 get :gantt
253 assert_response :success
253 assert_response :success
254 assert_template 'gantt.rhtml'
254 assert_template 'gantt.rhtml'
255 assert_not_nil assigns(:gantt)
255 assert_not_nil assigns(:gantt)
256 events = assigns(:gantt).events
256 events = assigns(:gantt).events
257 assert_not_nil events
257 assert_not_nil events
258 end
258 end
259
259
260 def test_gantt_export_to_pdf
260 def test_gantt_export_to_pdf
261 get :gantt, :project_id => 1, :format => 'pdf'
261 get :gantt, :project_id => 1, :format => 'pdf'
262 assert_response :success
262 assert_response :success
263 assert_equal 'application/pdf', @response.content_type
263 assert_equal 'application/pdf', @response.content_type
264 assert @response.body.starts_with?('%PDF')
264 assert @response.body.starts_with?('%PDF')
265 assert_not_nil assigns(:gantt)
265 assert_not_nil assigns(:gantt)
266 end
266 end
267
267
268 def test_cross_project_gantt_export_to_pdf
268 def test_cross_project_gantt_export_to_pdf
269 get :gantt, :format => 'pdf'
269 get :gantt, :format => 'pdf'
270 assert_response :success
270 assert_response :success
271 assert_equal 'application/pdf', @response.content_type
271 assert_equal 'application/pdf', @response.content_type
272 assert @response.body.starts_with?('%PDF')
272 assert @response.body.starts_with?('%PDF')
273 assert_not_nil assigns(:gantt)
273 assert_not_nil assigns(:gantt)
274 end
274 end
275
275
276 if Object.const_defined?(:Magick)
276 if Object.const_defined?(:Magick)
277 def test_gantt_image
277 def test_gantt_image
278 get :gantt, :project_id => 1, :format => 'png'
278 get :gantt, :project_id => 1, :format => 'png'
279 assert_response :success
279 assert_response :success
280 assert_equal 'image/png', @response.content_type
280 assert_equal 'image/png', @response.content_type
281 end
281 end
282 else
282 else
283 puts "RMagick not installed. Skipping tests !!!"
283 puts "RMagick not installed. Skipping tests !!!"
284 end
284 end
285
285
286 def test_calendar
286 def test_calendar
287 get :calendar, :project_id => 1
287 get :calendar, :project_id => 1
288 assert_response :success
288 assert_response :success
289 assert_template 'calendar'
289 assert_template 'calendar'
290 assert_not_nil assigns(:calendar)
290 assert_not_nil assigns(:calendar)
291 end
291 end
292
292
293 def test_cross_project_calendar
293 def test_cross_project_calendar
294 get :calendar
294 get :calendar
295 assert_response :success
295 assert_response :success
296 assert_template 'calendar'
296 assert_template 'calendar'
297 assert_not_nil assigns(:calendar)
297 assert_not_nil assigns(:calendar)
298 end
298 end
299
299
300 def test_changes
300 def test_changes
301 get :changes, :project_id => 1
301 get :changes, :project_id => 1
302 assert_response :success
302 assert_response :success
303 assert_not_nil assigns(:journals)
303 assert_not_nil assigns(:journals)
304 assert_equal 'application/atom+xml', @response.content_type
304 assert_equal 'application/atom+xml', @response.content_type
305 end
305 end
306
306
307 def test_show_by_anonymous
307 def test_show_by_anonymous
308 get :show, :id => 1
308 get :show, :id => 1
309 assert_response :success
309 assert_response :success
310 assert_template 'show.rhtml'
310 assert_template 'show.rhtml'
311 assert_not_nil assigns(:issue)
311 assert_not_nil assigns(:issue)
312 assert_equal Issue.find(1), assigns(:issue)
312 assert_equal Issue.find(1), assigns(:issue)
313
313
314 # anonymous role is allowed to add a note
314 # anonymous role is allowed to add a note
315 assert_tag :tag => 'form',
315 assert_tag :tag => 'form',
316 :descendant => { :tag => 'fieldset',
316 :descendant => { :tag => 'fieldset',
317 :child => { :tag => 'legend',
317 :child => { :tag => 'legend',
318 :content => /Notes/ } }
318 :content => /Notes/ } }
319 end
319 end
320
320
321 def test_show_by_manager
321 def test_show_by_manager
322 @request.session[:user_id] = 2
322 @request.session[:user_id] = 2
323 get :show, :id => 1
323 get :show, :id => 1
324 assert_response :success
324 assert_response :success
325
325
326 assert_tag :tag => 'form',
326 assert_tag :tag => 'form',
327 :descendant => { :tag => 'fieldset',
327 :descendant => { :tag => 'fieldset',
328 :child => { :tag => 'legend',
328 :child => { :tag => 'legend',
329 :content => /Change properties/ } },
329 :content => /Change properties/ } },
330 :descendant => { :tag => 'fieldset',
330 :descendant => { :tag => 'fieldset',
331 :child => { :tag => 'legend',
331 :child => { :tag => 'legend',
332 :content => /Log time/ } },
332 :content => /Log time/ } },
333 :descendant => { :tag => 'fieldset',
333 :descendant => { :tag => 'fieldset',
334 :child => { :tag => 'legend',
334 :child => { :tag => 'legend',
335 :content => /Notes/ } }
335 :content => /Notes/ } }
336 end
336 end
337
337
338 def test_show_should_deny_anonymous_access_without_permission
338 def test_show_should_deny_anonymous_access_without_permission
339 Role.anonymous.remove_permission!(:view_issues)
339 Role.anonymous.remove_permission!(:view_issues)
340 get :show, :id => 1
340 get :show, :id => 1
341 assert_response :redirect
341 assert_response :redirect
342 end
342 end
343
343
344 def test_show_should_deny_non_member_access_without_permission
344 def test_show_should_deny_non_member_access_without_permission
345 Role.non_member.remove_permission!(:view_issues)
345 Role.non_member.remove_permission!(:view_issues)
346 @request.session[:user_id] = 9
346 @request.session[:user_id] = 9
347 get :show, :id => 1
347 get :show, :id => 1
348 assert_response 403
348 assert_response 403
349 end
349 end
350
350
351 def test_show_should_deny_member_access_without_permission
351 def test_show_should_deny_member_access_without_permission
352 Role.find(1).remove_permission!(:view_issues)
352 Role.find(1).remove_permission!(:view_issues)
353 @request.session[:user_id] = 2
353 @request.session[:user_id] = 2
354 get :show, :id => 1
354 get :show, :id => 1
355 assert_response 403
355 assert_response 403
356 end
356 end
357
357
358 def test_show_should_not_disclose_relations_to_invisible_issues
358 def test_show_should_not_disclose_relations_to_invisible_issues
359 Setting.cross_project_issue_relations = '1'
359 Setting.cross_project_issue_relations = '1'
360 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(2), :relation_type => 'relates')
360 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(2), :relation_type => 'relates')
361 # Relation to a private project issue
361 # Relation to a private project issue
362 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(4), :relation_type => 'relates')
362 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(4), :relation_type => 'relates')
363
363
364 get :show, :id => 1
364 get :show, :id => 1
365 assert_response :success
365 assert_response :success
366
366
367 assert_tag :div, :attributes => { :id => 'relations' },
367 assert_tag :div, :attributes => { :id => 'relations' },
368 :descendant => { :tag => 'a', :content => /#2$/ }
368 :descendant => { :tag => 'a', :content => /#2$/ }
369 assert_no_tag :div, :attributes => { :id => 'relations' },
369 assert_no_tag :div, :attributes => { :id => 'relations' },
370 :descendant => { :tag => 'a', :content => /#4$/ }
370 :descendant => { :tag => 'a', :content => /#4$/ }
371 end
371 end
372
372
373 def test_show_atom
373 def test_show_atom
374 get :show, :id => 2, :format => 'atom'
374 get :show, :id => 2, :format => 'atom'
375 assert_response :success
375 assert_response :success
376 assert_template 'changes.rxml'
376 assert_template 'changes.rxml'
377 # Inline image
377 # Inline image
378 assert @response.body.include?("&lt;img src=\"http://test.host/attachments/download/10\" alt=\"\" /&gt;"), "Body did not match. Body: #{@response.body}"
378 assert @response.body.include?("&lt;img src=\"http://test.host/attachments/download/10\" alt=\"\" /&gt;"), "Body did not match. Body: #{@response.body}"
379 end
379 end
380
380
381 def test_show_export_to_pdf
381 def test_show_export_to_pdf
382 get :show, :id => 3, :format => 'pdf'
382 get :show, :id => 3, :format => 'pdf'
383 assert_response :success
383 assert_response :success
384 assert_equal 'application/pdf', @response.content_type
384 assert_equal 'application/pdf', @response.content_type
385 assert @response.body.starts_with?('%PDF')
385 assert @response.body.starts_with?('%PDF')
386 assert_not_nil assigns(:issue)
386 assert_not_nil assigns(:issue)
387 end
387 end
388
388
389 def test_get_new
389 def test_get_new
390 @request.session[:user_id] = 2
390 @request.session[:user_id] = 2
391 get :new, :project_id => 1, :tracker_id => 1
391 get :new, :project_id => 1, :tracker_id => 1
392 assert_response :success
392 assert_response :success
393 assert_template 'new'
393 assert_template 'new'
394
394
395 assert_tag :tag => 'input', :attributes => { :name => 'issue[custom_field_values][2]',
395 assert_tag :tag => 'input', :attributes => { :name => 'issue[custom_field_values][2]',
396 :value => 'Default string' }
396 :value => 'Default string' }
397 end
397 end
398
398
399 def test_get_new_without_tracker_id
399 def test_get_new_without_tracker_id
400 @request.session[:user_id] = 2
400 @request.session[:user_id] = 2
401 get :new, :project_id => 1
401 get :new, :project_id => 1
402 assert_response :success
402 assert_response :success
403 assert_template 'new'
403 assert_template 'new'
404
404
405 issue = assigns(:issue)
405 issue = assigns(:issue)
406 assert_not_nil issue
406 assert_not_nil issue
407 assert_equal Project.find(1).trackers.first, issue.tracker
407 assert_equal Project.find(1).trackers.first, issue.tracker
408 end
408 end
409
409
410 def test_get_new_with_no_default_status_should_display_an_error
410 def test_get_new_with_no_default_status_should_display_an_error
411 @request.session[:user_id] = 2
411 @request.session[:user_id] = 2
412 IssueStatus.delete_all
412 IssueStatus.delete_all
413
413
414 get :new, :project_id => 1
414 get :new, :project_id => 1
415 assert_response 500
415 assert_response 500
416 assert_not_nil flash[:error]
416 assert_not_nil flash[:error]
417 assert_tag :tag => 'div', :attributes => { :class => /error/ },
417 assert_tag :tag => 'div', :attributes => { :class => /error/ },
418 :content => /No default issue/
418 :content => /No default issue/
419 end
419 end
420
420
421 def test_get_new_with_no_tracker_should_display_an_error
421 def test_get_new_with_no_tracker_should_display_an_error
422 @request.session[:user_id] = 2
422 @request.session[:user_id] = 2
423 Tracker.delete_all
423 Tracker.delete_all
424
424
425 get :new, :project_id => 1
425 get :new, :project_id => 1
426 assert_response 500
426 assert_response 500
427 assert_not_nil flash[:error]
427 assert_not_nil flash[:error]
428 assert_tag :tag => 'div', :attributes => { :class => /error/ },
428 assert_tag :tag => 'div', :attributes => { :class => /error/ },
429 :content => /No tracker/
429 :content => /No tracker/
430 end
430 end
431
431
432 def test_update_new_form
432 def test_update_new_form
433 @request.session[:user_id] = 2
433 @request.session[:user_id] = 2
434 xhr :post, :update_form, :project_id => 1,
434 xhr :post, :update_form, :project_id => 1,
435 :issue => {:tracker_id => 2,
435 :issue => {:tracker_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 assert_response :success
439 assert_response :success
440 assert_template 'attributes'
440 assert_template 'attributes'
441
441
442 issue = assigns(:issue)
442 issue = assigns(:issue)
443 assert_kind_of Issue, issue
443 assert_kind_of Issue, issue
444 assert_equal 1, issue.project_id
444 assert_equal 1, issue.project_id
445 assert_equal 2, issue.tracker_id
445 assert_equal 2, issue.tracker_id
446 assert_equal 'This is the test_new issue', issue.subject
446 assert_equal 'This is the test_new issue', issue.subject
447 end
447 end
448
448
449 def test_post_new
449 def test_post_new
450 @request.session[:user_id] = 2
450 @request.session[:user_id] = 2
451 assert_difference 'Issue.count' do
451 assert_difference 'Issue.count' do
452 post :new, :project_id => 1,
452 post :new, :project_id => 1,
453 :issue => {:tracker_id => 3,
453 :issue => {:tracker_id => 3,
454 :subject => 'This is the test_new issue',
454 :subject => 'This is the test_new issue',
455 :description => 'This is the description',
455 :description => 'This is the description',
456 :priority_id => 5,
456 :priority_id => 5,
457 :estimated_hours => '',
457 :estimated_hours => '',
458 :custom_field_values => {'2' => 'Value for field 2'}}
458 :custom_field_values => {'2' => 'Value for field 2'}}
459 end
459 end
460 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
460 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
461
461
462 issue = Issue.find_by_subject('This is the test_new issue')
462 issue = Issue.find_by_subject('This is the test_new issue')
463 assert_not_nil issue
463 assert_not_nil issue
464 assert_equal 2, issue.author_id
464 assert_equal 2, issue.author_id
465 assert_equal 3, issue.tracker_id
465 assert_equal 3, issue.tracker_id
466 assert_nil issue.estimated_hours
466 assert_nil issue.estimated_hours
467 v = issue.custom_values.find(:first, :conditions => {:custom_field_id => 2})
467 v = issue.custom_values.find(:first, :conditions => {:custom_field_id => 2})
468 assert_not_nil v
468 assert_not_nil v
469 assert_equal 'Value for field 2', v.value
469 assert_equal 'Value for field 2', v.value
470 end
470 end
471
471
472 def test_post_new_and_continue
472 def test_post_new_and_continue
473 @request.session[:user_id] = 2
473 @request.session[:user_id] = 2
474 post :new, :project_id => 1,
474 post :new, :project_id => 1,
475 :issue => {:tracker_id => 3,
475 :issue => {:tracker_id => 3,
476 :subject => 'This is first issue',
476 :subject => 'This is first issue',
477 :priority_id => 5},
477 :priority_id => 5},
478 :continue => ''
478 :continue => ''
479 assert_redirected_to :controller => 'issues', :action => 'new', :tracker_id => 3
479 assert_redirected_to :controller => 'issues', :action => 'new', :tracker_id => 3
480 end
480 end
481
481
482 def test_post_new_without_custom_fields_param
482 def test_post_new_without_custom_fields_param
483 @request.session[:user_id] = 2
483 @request.session[:user_id] = 2
484 assert_difference 'Issue.count' do
484 assert_difference 'Issue.count' do
485 post :new, :project_id => 1,
485 post :new, :project_id => 1,
486 :issue => {:tracker_id => 1,
486 :issue => {:tracker_id => 1,
487 :subject => 'This is the test_new issue',
487 :subject => 'This is the test_new issue',
488 :description => 'This is the description',
488 :description => 'This is the description',
489 :priority_id => 5}
489 :priority_id => 5}
490 end
490 end
491 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
491 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
492 end
492 end
493
493
494 def test_post_new_with_required_custom_field_and_without_custom_fields_param
494 def test_post_new_with_required_custom_field_and_without_custom_fields_param
495 field = IssueCustomField.find_by_name('Database')
495 field = IssueCustomField.find_by_name('Database')
496 field.update_attribute(:is_required, true)
496 field.update_attribute(:is_required, true)
497
497
498 @request.session[:user_id] = 2
498 @request.session[:user_id] = 2
499 post :new, :project_id => 1,
499 post :new, :project_id => 1,
500 :issue => {:tracker_id => 1,
500 :issue => {:tracker_id => 1,
501 :subject => 'This is the test_new issue',
501 :subject => 'This is the test_new issue',
502 :description => 'This is the description',
502 :description => 'This is the description',
503 :priority_id => 5}
503 :priority_id => 5}
504 assert_response :success
504 assert_response :success
505 assert_template 'new'
505 assert_template 'new'
506 issue = assigns(:issue)
506 issue = assigns(:issue)
507 assert_not_nil issue
507 assert_not_nil issue
508 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
508 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
509 end
509 end
510
510
511 def test_post_new_with_watchers
511 def test_post_new_with_watchers
512 @request.session[:user_id] = 2
512 @request.session[:user_id] = 2
513 ActionMailer::Base.deliveries.clear
513 ActionMailer::Base.deliveries.clear
514
514
515 assert_difference 'Watcher.count', 2 do
515 assert_difference 'Watcher.count', 2 do
516 post :new, :project_id => 1,
516 post :new, :project_id => 1,
517 :issue => {:tracker_id => 1,
517 :issue => {:tracker_id => 1,
518 :subject => 'This is a new issue with watchers',
518 :subject => 'This is a new issue with watchers',
519 :description => 'This is the description',
519 :description => 'This is the description',
520 :priority_id => 5,
520 :priority_id => 5,
521 :watcher_user_ids => ['2', '3']}
521 :watcher_user_ids => ['2', '3']}
522 end
522 end
523 issue = Issue.find_by_subject('This is a new issue with watchers')
523 issue = Issue.find_by_subject('This is a new issue with watchers')
524 assert_not_nil issue
524 assert_not_nil issue
525 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
525 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
526
526
527 # Watchers added
527 # Watchers added
528 assert_equal [2, 3], issue.watcher_user_ids.sort
528 assert_equal [2, 3], issue.watcher_user_ids.sort
529 assert issue.watched_by?(User.find(3))
529 assert issue.watched_by?(User.find(3))
530 # Watchers notified
530 # Watchers notified
531 mail = ActionMailer::Base.deliveries.last
531 mail = ActionMailer::Base.deliveries.last
532 assert_kind_of TMail::Mail, mail
532 assert_kind_of TMail::Mail, mail
533 assert [mail.bcc, mail.cc].flatten.include?(User.find(3).mail)
533 assert [mail.bcc, mail.cc].flatten.include?(User.find(3).mail)
534 end
534 end
535
535
536 def test_post_new_should_send_a_notification
536 def test_post_new_should_send_a_notification
537 ActionMailer::Base.deliveries.clear
537 ActionMailer::Base.deliveries.clear
538 @request.session[:user_id] = 2
538 @request.session[:user_id] = 2
539 assert_difference 'Issue.count' do
539 assert_difference 'Issue.count' do
540 post :new, :project_id => 1,
540 post :new, :project_id => 1,
541 :issue => {:tracker_id => 3,
541 :issue => {:tracker_id => 3,
542 :subject => 'This is the test_new issue',
542 :subject => 'This is the test_new issue',
543 :description => 'This is the description',
543 :description => 'This is the description',
544 :priority_id => 5,
544 :priority_id => 5,
545 :estimated_hours => '',
545 :estimated_hours => '',
546 :custom_field_values => {'2' => 'Value for field 2'}}
546 :custom_field_values => {'2' => 'Value for field 2'}}
547 end
547 end
548 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
548 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
549
549
550 assert_equal 1, ActionMailer::Base.deliveries.size
550 assert_equal 1, ActionMailer::Base.deliveries.size
551 end
551 end
552
552
553 def test_post_should_preserve_fields_values_on_validation_failure
553 def test_post_should_preserve_fields_values_on_validation_failure
554 @request.session[:user_id] = 2
554 @request.session[:user_id] = 2
555 post :new, :project_id => 1,
555 post :new, :project_id => 1,
556 :issue => {:tracker_id => 1,
556 :issue => {:tracker_id => 1,
557 # empty subject
557 # empty subject
558 :subject => '',
558 :subject => '',
559 :description => 'This is a description',
559 :description => 'This is a description',
560 :priority_id => 6,
560 :priority_id => 6,
561 :custom_field_values => {'1' => 'Oracle', '2' => 'Value for field 2'}}
561 :custom_field_values => {'1' => 'Oracle', '2' => 'Value for field 2'}}
562 assert_response :success
562 assert_response :success
563 assert_template 'new'
563 assert_template 'new'
564
564
565 assert_tag :textarea, :attributes => { :name => 'issue[description]' },
565 assert_tag :textarea, :attributes => { :name => 'issue[description]' },
566 :content => 'This is a description'
566 :content => 'This is a description'
567 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
567 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
568 :child => { :tag => 'option', :attributes => { :selected => 'selected',
568 :child => { :tag => 'option', :attributes => { :selected => 'selected',
569 :value => '6' },
569 :value => '6' },
570 :content => 'High' }
570 :content => 'High' }
571 # Custom fields
571 # Custom fields
572 assert_tag :select, :attributes => { :name => 'issue[custom_field_values][1]' },
572 assert_tag :select, :attributes => { :name => 'issue[custom_field_values][1]' },
573 :child => { :tag => 'option', :attributes => { :selected => 'selected',
573 :child => { :tag => 'option', :attributes => { :selected => 'selected',
574 :value => 'Oracle' },
574 :value => 'Oracle' },
575 :content => 'Oracle' }
575 :content => 'Oracle' }
576 assert_tag :input, :attributes => { :name => 'issue[custom_field_values][2]',
576 assert_tag :input, :attributes => { :name => 'issue[custom_field_values][2]',
577 :value => 'Value for field 2'}
577 :value => 'Value for field 2'}
578 end
578 end
579
579
580 def test_post_new_should_ignore_non_safe_attributes
580 def test_post_new_should_ignore_non_safe_attributes
581 @request.session[:user_id] = 2
581 @request.session[:user_id] = 2
582 assert_nothing_raised do
582 assert_nothing_raised do
583 post :new, :project_id => 1, :issue => { :tracker => "A param can not be a Tracker" }
583 post :new, :project_id => 1, :issue => { :tracker => "A param can not be a Tracker" }
584 end
584 end
585 end
585 end
586
586
587 def test_copy_issue
587 def test_copy_issue
588 @request.session[:user_id] = 2
588 @request.session[:user_id] = 2
589 get :new, :project_id => 1, :copy_from => 1
589 get :new, :project_id => 1, :copy_from => 1
590 assert_template 'new'
590 assert_template 'new'
591 assert_not_nil assigns(:issue)
591 assert_not_nil assigns(:issue)
592 orig = Issue.find(1)
592 orig = Issue.find(1)
593 assert_equal orig.subject, assigns(:issue).subject
593 assert_equal orig.subject, assigns(:issue).subject
594 end
594 end
595
595
596 def test_get_edit
596 def test_get_edit
597 @request.session[:user_id] = 2
597 @request.session[:user_id] = 2
598 get :edit, :id => 1
598 get :edit, :id => 1
599 assert_response :success
599 assert_response :success
600 assert_template 'edit'
600 assert_template 'edit'
601 assert_not_nil assigns(:issue)
601 assert_not_nil assigns(:issue)
602 assert_equal Issue.find(1), assigns(:issue)
602 assert_equal Issue.find(1), assigns(:issue)
603 end
603 end
604
604
605 def test_get_edit_with_params
605 def test_get_edit_with_params
606 @request.session[:user_id] = 2
606 @request.session[:user_id] = 2
607 get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 }
607 get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 }
608 assert_response :success
608 assert_response :success
609 assert_template 'edit'
609 assert_template 'edit'
610
610
611 issue = assigns(:issue)
611 issue = assigns(:issue)
612 assert_not_nil issue
612 assert_not_nil issue
613
613
614 assert_equal 5, issue.status_id
614 assert_equal 5, issue.status_id
615 assert_tag :select, :attributes => { :name => 'issue[status_id]' },
615 assert_tag :select, :attributes => { :name => 'issue[status_id]' },
616 :child => { :tag => 'option',
616 :child => { :tag => 'option',
617 :content => 'Closed',
617 :content => 'Closed',
618 :attributes => { :selected => 'selected' } }
618 :attributes => { :selected => 'selected' } }
619
619
620 assert_equal 7, issue.priority_id
620 assert_equal 7, issue.priority_id
621 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
621 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
622 :child => { :tag => 'option',
622 :child => { :tag => 'option',
623 :content => 'Urgent',
623 :content => 'Urgent',
624 :attributes => { :selected => 'selected' } }
624 :attributes => { :selected => 'selected' } }
625 end
625 end
626
626
627 def test_update_edit_form
627 def test_update_edit_form
628 @request.session[:user_id] = 2
628 @request.session[:user_id] = 2
629 xhr :post, :update_form, :project_id => 1,
629 xhr :post, :update_form, :project_id => 1,
630 :id => 1,
630 :id => 1,
631 :issue => {:tracker_id => 2,
631 :issue => {:tracker_id => 2,
632 :subject => 'This is the test_new issue',
632 :subject => 'This is the test_new issue',
633 :description => 'This is the description',
633 :description => 'This is the description',
634 :priority_id => 5}
634 :priority_id => 5}
635 assert_response :success
635 assert_response :success
636 assert_template 'attributes'
636 assert_template 'attributes'
637
637
638 issue = assigns(:issue)
638 issue = assigns(:issue)
639 assert_kind_of Issue, issue
639 assert_kind_of Issue, issue
640 assert_equal 1, issue.id
640 assert_equal 1, issue.id
641 assert_equal 1, issue.project_id
641 assert_equal 1, issue.project_id
642 assert_equal 2, issue.tracker_id
642 assert_equal 2, issue.tracker_id
643 assert_equal 'This is the test_new issue', issue.subject
643 assert_equal 'This is the test_new issue', issue.subject
644 end
644 end
645
645
646 def test_reply_to_issue
646 def test_reply_to_issue
647 @request.session[:user_id] = 2
647 @request.session[:user_id] = 2
648 get :reply, :id => 1
648 get :reply, :id => 1
649 assert_response :success
649 assert_response :success
650 assert_select_rjs :show, "update"
650 assert_select_rjs :show, "update"
651 end
651 end
652
652
653 def test_reply_to_note
653 def test_reply_to_note
654 @request.session[:user_id] = 2
654 @request.session[:user_id] = 2
655 get :reply, :id => 1, :journal_id => 2
655 get :reply, :id => 1, :journal_id => 2
656 assert_response :success
656 assert_response :success
657 assert_select_rjs :show, "update"
657 assert_select_rjs :show, "update"
658 end
658 end
659
659
660 def test_post_edit_without_custom_fields_param
660 def test_post_edit_without_custom_fields_param
661 @request.session[:user_id] = 2
661 @request.session[:user_id] = 2
662 ActionMailer::Base.deliveries.clear
662 ActionMailer::Base.deliveries.clear
663
663
664 issue = Issue.find(1)
664 issue = Issue.find(1)
665 assert_equal '125', issue.custom_value_for(2).value
665 assert_equal '125', issue.custom_value_for(2).value
666 old_subject = issue.subject
666 old_subject = issue.subject
667 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
667 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
668
668
669 assert_difference('Journal.count') do
669 assert_difference('Journal.count') do
670 assert_difference('JournalDetail.count', 2) do
670 assert_difference('JournalDetail.count', 2) do
671 post :edit, :id => 1, :issue => {:subject => new_subject,
671 post :edit, :id => 1, :issue => {:subject => new_subject,
672 :priority_id => '6',
672 :priority_id => '6',
673 :category_id => '1' # no change
673 :category_id => '1' # no change
674 }
674 }
675 end
675 end
676 end
676 end
677 assert_redirected_to :action => 'show', :id => '1'
677 assert_redirected_to :action => 'show', :id => '1'
678 issue.reload
678 issue.reload
679 assert_equal new_subject, issue.subject
679 assert_equal new_subject, issue.subject
680 # Make sure custom fields were not cleared
680 # Make sure custom fields were not cleared
681 assert_equal '125', issue.custom_value_for(2).value
681 assert_equal '125', issue.custom_value_for(2).value
682
682
683 mail = ActionMailer::Base.deliveries.last
683 mail = ActionMailer::Base.deliveries.last
684 assert_kind_of TMail::Mail, mail
684 assert_kind_of TMail::Mail, mail
685 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
685 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
686 assert mail.body.include?("Subject changed from #{old_subject} to #{new_subject}")
686 assert mail.body.include?("Subject changed from #{old_subject} to #{new_subject}")
687 end
687 end
688
688
689 def test_post_edit_with_custom_field_change
689 def test_post_edit_with_custom_field_change
690 @request.session[:user_id] = 2
690 @request.session[:user_id] = 2
691 issue = Issue.find(1)
691 issue = Issue.find(1)
692 assert_equal '125', issue.custom_value_for(2).value
692 assert_equal '125', issue.custom_value_for(2).value
693
693
694 assert_difference('Journal.count') do
694 assert_difference('Journal.count') do
695 assert_difference('JournalDetail.count', 3) do
695 assert_difference('JournalDetail.count', 3) do
696 post :edit, :id => 1, :issue => {:subject => 'Custom field change',
696 post :edit, :id => 1, :issue => {:subject => 'Custom field change',
697 :priority_id => '6',
697 :priority_id => '6',
698 :category_id => '1', # no change
698 :category_id => '1', # no change
699 :custom_field_values => { '2' => 'New custom value' }
699 :custom_field_values => { '2' => 'New custom value' }
700 }
700 }
701 end
701 end
702 end
702 end
703 assert_redirected_to :action => 'show', :id => '1'
703 assert_redirected_to :action => 'show', :id => '1'
704 issue.reload
704 issue.reload
705 assert_equal 'New custom value', issue.custom_value_for(2).value
705 assert_equal 'New custom value', issue.custom_value_for(2).value
706
706
707 mail = ActionMailer::Base.deliveries.last
707 mail = ActionMailer::Base.deliveries.last
708 assert_kind_of TMail::Mail, mail
708 assert_kind_of TMail::Mail, mail
709 assert mail.body.include?("Searchable field changed from 125 to New custom value")
709 assert mail.body.include?("Searchable field changed from 125 to New custom value")
710 end
710 end
711
711
712 def test_post_edit_with_status_and_assignee_change
712 def test_post_edit_with_status_and_assignee_change
713 issue = Issue.find(1)
713 issue = Issue.find(1)
714 assert_equal 1, issue.status_id
714 assert_equal 1, issue.status_id
715 @request.session[:user_id] = 2
715 @request.session[:user_id] = 2
716 assert_difference('TimeEntry.count', 0) do
716 assert_difference('TimeEntry.count', 0) do
717 post :edit,
717 post :edit,
718 :id => 1,
718 :id => 1,
719 :issue => { :status_id => 2, :assigned_to_id => 3 },
719 :issue => { :status_id => 2, :assigned_to_id => 3 },
720 :notes => 'Assigned to dlopper',
720 :notes => 'Assigned to dlopper',
721 :time_entry => { :hours => '', :comments => '', :activity_id => TimeEntryActivity.first }
721 :time_entry => { :hours => '', :comments => '', :activity_id => TimeEntryActivity.first }
722 end
722 end
723 assert_redirected_to :action => 'show', :id => '1'
723 assert_redirected_to :action => 'show', :id => '1'
724 issue.reload
724 issue.reload
725 assert_equal 2, issue.status_id
725 assert_equal 2, issue.status_id
726 j = Journal.find(:first, :order => 'id DESC')
726 j = Journal.find(:first, :order => 'id DESC')
727 assert_equal 'Assigned to dlopper', j.notes
727 assert_equal 'Assigned to dlopper', j.notes
728 assert_equal 2, j.details.size
728 assert_equal 2, j.details.size
729
729
730 mail = ActionMailer::Base.deliveries.last
730 mail = ActionMailer::Base.deliveries.last
731 assert mail.body.include?("Status changed from New to Assigned")
731 assert mail.body.include?("Status changed from New to Assigned")
732 # subject should contain the new status
732 # subject should contain the new status
733 assert mail.subject.include?("(#{ IssueStatus.find(2).name })")
733 assert mail.subject.include?("(#{ IssueStatus.find(2).name })")
734 end
734 end
735
735
736 def test_post_edit_with_note_only
736 def test_post_edit_with_note_only
737 notes = 'Note added by IssuesControllerTest#test_update_with_note_only'
737 notes = 'Note added by IssuesControllerTest#test_update_with_note_only'
738 # anonymous user
738 # anonymous user
739 post :edit,
739 post :edit,
740 :id => 1,
740 :id => 1,
741 :notes => notes
741 :notes => notes
742 assert_redirected_to :action => 'show', :id => '1'
742 assert_redirected_to :action => 'show', :id => '1'
743 j = Journal.find(:first, :order => 'id DESC')
743 j = Journal.find(:first, :order => 'id DESC')
744 assert_equal notes, j.notes
744 assert_equal notes, j.notes
745 assert_equal 0, j.details.size
745 assert_equal 0, j.details.size
746 assert_equal User.anonymous, j.user
746 assert_equal User.anonymous, j.user
747
747
748 mail = ActionMailer::Base.deliveries.last
748 mail = ActionMailer::Base.deliveries.last
749 assert mail.body.include?(notes)
749 assert mail.body.include?(notes)
750 end
750 end
751
751
752 def test_post_edit_with_note_and_spent_time
752 def test_post_edit_with_note_and_spent_time
753 @request.session[:user_id] = 2
753 @request.session[:user_id] = 2
754 spent_hours_before = Issue.find(1).spent_hours
754 spent_hours_before = Issue.find(1).spent_hours
755 assert_difference('TimeEntry.count') do
755 assert_difference('TimeEntry.count') do
756 post :edit,
756 post :edit,
757 :id => 1,
757 :id => 1,
758 :notes => '2.5 hours added',
758 :notes => '2.5 hours added',
759 :time_entry => { :hours => '2.5', :comments => '', :activity_id => TimeEntryActivity.first }
759 :time_entry => { :hours => '2.5', :comments => '', :activity_id => TimeEntryActivity.first }
760 end
760 end
761 assert_redirected_to :action => 'show', :id => '1'
761 assert_redirected_to :action => 'show', :id => '1'
762
762
763 issue = Issue.find(1)
763 issue = Issue.find(1)
764
764
765 j = Journal.find(:first, :order => 'id DESC')
765 j = Journal.find(:first, :order => 'id DESC')
766 assert_equal '2.5 hours added', j.notes
766 assert_equal '2.5 hours added', j.notes
767 assert_equal 0, j.details.size
767 assert_equal 0, j.details.size
768
768
769 t = issue.time_entries.find(:first, :order => 'id DESC')
769 t = issue.time_entries.find(:first, :order => 'id DESC')
770 assert_not_nil t
770 assert_not_nil t
771 assert_equal 2.5, t.hours
771 assert_equal 2.5, t.hours
772 assert_equal spent_hours_before + 2.5, issue.spent_hours
772 assert_equal spent_hours_before + 2.5, issue.spent_hours
773 end
773 end
774
774
775 def test_post_edit_with_attachment_only
775 def test_post_edit_with_attachment_only
776 set_tmp_attachments_directory
776 set_tmp_attachments_directory
777
777
778 # Delete all fixtured journals, a race condition can occur causing the wrong
778 # Delete all fixtured journals, a race condition can occur causing the wrong
779 # journal to get fetched in the next find.
779 # journal to get fetched in the next find.
780 Journal.delete_all
780 Journal.delete_all
781
781
782 # anonymous user
782 # anonymous user
783 post :edit,
783 post :edit,
784 :id => 1,
784 :id => 1,
785 :notes => '',
785 :notes => '',
786 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
786 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
787 assert_redirected_to :action => 'show', :id => '1'
787 assert_redirected_to :action => 'show', :id => '1'
788 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
788 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
789 assert j.notes.blank?
789 assert j.notes.blank?
790 assert_equal 1, j.details.size
790 assert_equal 1, j.details.size
791 assert_equal 'testfile.txt', j.details.first.value
791 assert_equal 'testfile.txt', j.details.first.value
792 assert_equal User.anonymous, j.user
792 assert_equal User.anonymous, j.user
793
793
794 mail = ActionMailer::Base.deliveries.last
794 mail = ActionMailer::Base.deliveries.last
795 assert mail.body.include?('testfile.txt')
795 assert mail.body.include?('testfile.txt')
796 end
796 end
797
797
798 def test_post_edit_with_no_change
798 def test_post_edit_with_no_change
799 issue = Issue.find(1)
799 issue = Issue.find(1)
800 issue.journals.clear
800 issue.journals.clear
801 ActionMailer::Base.deliveries.clear
801 ActionMailer::Base.deliveries.clear
802
802
803 post :edit,
803 post :edit,
804 :id => 1,
804 :id => 1,
805 :notes => ''
805 :notes => ''
806 assert_redirected_to :action => 'show', :id => '1'
806 assert_redirected_to :action => 'show', :id => '1'
807
807
808 issue.reload
808 issue.reload
809 assert issue.journals.empty?
809 assert issue.journals.empty?
810 # No email should be sent
810 # No email should be sent
811 assert ActionMailer::Base.deliveries.empty?
811 assert ActionMailer::Base.deliveries.empty?
812 end
812 end
813
813
814 def test_post_edit_should_send_a_notification
814 def test_post_edit_should_send_a_notification
815 @request.session[:user_id] = 2
815 @request.session[:user_id] = 2
816 ActionMailer::Base.deliveries.clear
816 ActionMailer::Base.deliveries.clear
817 issue = Issue.find(1)
817 issue = Issue.find(1)
818 old_subject = issue.subject
818 old_subject = issue.subject
819 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
819 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
820
820
821 post :edit, :id => 1, :issue => {:subject => new_subject,
821 post :edit, :id => 1, :issue => {:subject => new_subject,
822 :priority_id => '6',
822 :priority_id => '6',
823 :category_id => '1' # no change
823 :category_id => '1' # no change
824 }
824 }
825 assert_equal 1, ActionMailer::Base.deliveries.size
825 assert_equal 1, ActionMailer::Base.deliveries.size
826 end
826 end
827
827
828 def test_post_edit_with_invalid_spent_time
828 def test_post_edit_with_invalid_spent_time
829 @request.session[:user_id] = 2
829 @request.session[:user_id] = 2
830 notes = 'Note added by IssuesControllerTest#test_post_edit_with_invalid_spent_time'
830 notes = 'Note added by IssuesControllerTest#test_post_edit_with_invalid_spent_time'
831
831
832 assert_no_difference('Journal.count') do
832 assert_no_difference('Journal.count') do
833 post :edit,
833 post :edit,
834 :id => 1,
834 :id => 1,
835 :notes => notes,
835 :notes => notes,
836 :time_entry => {"comments"=>"", "activity_id"=>"", "hours"=>"2z"}
836 :time_entry => {"comments"=>"", "activity_id"=>"", "hours"=>"2z"}
837 end
837 end
838 assert_response :success
838 assert_response :success
839 assert_template 'edit'
839 assert_template 'edit'
840
840
841 assert_tag :textarea, :attributes => { :name => 'notes' },
841 assert_tag :textarea, :attributes => { :name => 'notes' },
842 :content => notes
842 :content => notes
843 assert_tag :input, :attributes => { :name => 'time_entry[hours]', :value => "2z" }
843 assert_tag :input, :attributes => { :name => 'time_entry[hours]', :value => "2z" }
844 end
844 end
845
845
846 def test_post_edit_should_allow_fixed_version_to_be_set_to_a_subproject
846 def test_post_edit_should_allow_fixed_version_to_be_set_to_a_subproject
847 issue = Issue.find(2)
847 issue = Issue.find(2)
848 @request.session[:user_id] = 2
848 @request.session[:user_id] = 2
849
849
850 post :edit,
850 post :edit,
851 :id => issue.id,
851 :id => issue.id,
852 :issue => {
852 :issue => {
853 :fixed_version_id => 4
853 :fixed_version_id => 4
854 }
854 }
855
855
856 assert_response :redirect
856 assert_response :redirect
857 issue.reload
857 issue.reload
858 assert_equal 4, issue.fixed_version_id
858 assert_equal 4, issue.fixed_version_id
859 assert_not_equal issue.project_id, issue.fixed_version.project_id
859 assert_not_equal issue.project_id, issue.fixed_version.project_id
860 end
860 end
861
861
862 def test_post_edit_should_redirect_back_using_the_back_url_parameter
862 def test_post_edit_should_redirect_back_using_the_back_url_parameter
863 issue = Issue.find(2)
863 issue = Issue.find(2)
864 @request.session[:user_id] = 2
864 @request.session[:user_id] = 2
865
865
866 post :edit,
866 post :edit,
867 :id => issue.id,
867 :id => issue.id,
868 :issue => {
868 :issue => {
869 :fixed_version_id => 4
869 :fixed_version_id => 4
870 },
870 },
871 :back_url => '/issues'
871 :back_url => '/issues'
872
872
873 assert_response :redirect
873 assert_response :redirect
874 assert_redirected_to '/issues'
874 assert_redirected_to '/issues'
875 end
875 end
876
876
877 def test_post_edit_should_not_redirect_back_using_the_back_url_parameter_off_the_host
877 def test_post_edit_should_not_redirect_back_using_the_back_url_parameter_off_the_host
878 issue = Issue.find(2)
878 issue = Issue.find(2)
879 @request.session[:user_id] = 2
879 @request.session[:user_id] = 2
880
880
881 post :edit,
881 post :edit,
882 :id => issue.id,
882 :id => issue.id,
883 :issue => {
883 :issue => {
884 :fixed_version_id => 4
884 :fixed_version_id => 4
885 },
885 },
886 :back_url => 'http://google.com'
886 :back_url => 'http://google.com'
887
887
888 assert_response :redirect
888 assert_response :redirect
889 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue.id
889 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue.id
890 end
890 end
891
891
892 def test_get_bulk_edit
892 def test_get_bulk_edit
893 @request.session[:user_id] = 2
893 @request.session[:user_id] = 2
894 get :bulk_edit, :ids => [1, 2]
894 get :bulk_edit, :ids => [1, 2]
895 assert_response :success
895 assert_response :success
896 assert_template 'bulk_edit'
896 assert_template 'bulk_edit'
897
897
898 # Project specific custom field, date type
898 # Project specific custom field, date type
899 field = CustomField.find(9)
899 field = CustomField.find(9)
900 assert !field.is_for_all?
900 assert !field.is_for_all?
901 assert_equal 'date', field.field_format
901 assert_equal 'date', field.field_format
902 assert_tag :input, :attributes => {:name => 'custom_field_values[9]'}
902 assert_tag :input, :attributes => {:name => 'issue[custom_field_values][9]'}
903
903
904 # System wide custom field
904 # System wide custom field
905 assert CustomField.find(1).is_for_all?
905 assert CustomField.find(1).is_for_all?
906 assert_tag :select, :attributes => {:name => 'custom_field_values[1]'}
906 assert_tag :select, :attributes => {:name => 'issue[custom_field_values][1]'}
907 end
907 end
908
908
909 def test_bulk_edit
909 def test_bulk_edit
910 @request.session[:user_id] = 2
910 @request.session[:user_id] = 2
911 # update issues priority
911 # update issues priority
912 post :bulk_edit, :ids => [1, 2], :priority_id => 7,
912 post :bulk_edit, :ids => [1, 2], :notes => 'Bulk editing',
913 :assigned_to_id => '',
913 :issue => {:priority_id => 7,
914 :custom_field_values => {'2' => ''},
914 :assigned_to_id => '',
915 :notes => 'Bulk editing'
915 :custom_field_values => {'2' => ''}}
916
916 assert_response 302
917 assert_response 302
917 # check that the issues were updated
918 # check that the issues were updated
918 assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id}
919 assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id}
919
920
920 issue = Issue.find(1)
921 issue = Issue.find(1)
921 journal = issue.journals.find(:first, :order => 'created_on DESC')
922 journal = issue.journals.find(:first, :order => 'created_on DESC')
922 assert_equal '125', issue.custom_value_for(2).value
923 assert_equal '125', issue.custom_value_for(2).value
923 assert_equal 'Bulk editing', journal.notes
924 assert_equal 'Bulk editing', journal.notes
924 assert_equal 1, journal.details.size
925 assert_equal 1, journal.details.size
925 end
926 end
926
927
927 def test_bullk_edit_should_send_a_notification
928 def test_bullk_edit_should_send_a_notification
928 @request.session[:user_id] = 2
929 @request.session[:user_id] = 2
929 ActionMailer::Base.deliveries.clear
930 ActionMailer::Base.deliveries.clear
930 post(:bulk_edit,
931 post(:bulk_edit,
931 {
932 {
932 :ids => [1, 2],
933 :ids => [1, 2],
933 :priority_id => 7,
934 :notes => 'Bulk editing',
934 :assigned_to_id => '',
935 :issue => {
935 :custom_field_values => {'2' => ''},
936 :priority_id => 7,
936 :notes => 'Bulk editing'
937 :assigned_to_id => '',
938 :custom_field_values => {'2' => ''}
939 }
937 })
940 })
938
941
939 assert_response 302
942 assert_response 302
940 assert_equal 2, ActionMailer::Base.deliveries.size
943 assert_equal 2, ActionMailer::Base.deliveries.size
941 end
944 end
942
945
943 def test_bulk_edit_status
946 def test_bulk_edit_status
944 @request.session[:user_id] = 2
947 @request.session[:user_id] = 2
945 # update issues priority
948 # update issues priority
946 post :bulk_edit, :ids => [1, 2], :priority_id => '',
949 post :bulk_edit, :ids => [1, 2], :notes => 'Bulk editing status',
947 :assigned_to_id => '',
950 :issue => {:priority_id => '',
948 :status_id => '5',
951 :assigned_to_id => '',
949 :notes => 'Bulk editing status'
952 :status_id => '5'}
953
950 assert_response 302
954 assert_response 302
951 issue = Issue.find(1)
955 issue = Issue.find(1)
952 assert issue.closed?
956 assert issue.closed?
953 end
957 end
954
958
955 def test_bulk_edit_custom_field
959 def test_bulk_edit_custom_field
956 @request.session[:user_id] = 2
960 @request.session[:user_id] = 2
957 # update issues priority
961 # update issues priority
958 post :bulk_edit, :ids => [1, 2], :priority_id => '',
962 post :bulk_edit, :ids => [1, 2], :notes => 'Bulk editing custom field',
959 :assigned_to_id => '',
963 :issue => {:priority_id => '',
960 :custom_field_values => {'2' => '777'},
964 :assigned_to_id => '',
961 :notes => 'Bulk editing custom field'
965 :custom_field_values => {'2' => '777'}}
966
962 assert_response 302
967 assert_response 302
963
968
964 issue = Issue.find(1)
969 issue = Issue.find(1)
965 journal = issue.journals.find(:first, :order => 'created_on DESC')
970 journal = issue.journals.find(:first, :order => 'created_on DESC')
966 assert_equal '777', issue.custom_value_for(2).value
971 assert_equal '777', issue.custom_value_for(2).value
967 assert_equal 1, journal.details.size
972 assert_equal 1, journal.details.size
968 assert_equal '125', journal.details.first.old_value
973 assert_equal '125', journal.details.first.old_value
969 assert_equal '777', journal.details.first.value
974 assert_equal '777', journal.details.first.value
970 end
975 end
971
976
972 def test_bulk_unassign
977 def test_bulk_unassign
973 assert_not_nil Issue.find(2).assigned_to
978 assert_not_nil Issue.find(2).assigned_to
974 @request.session[:user_id] = 2
979 @request.session[:user_id] = 2
975 # unassign issues
980 # unassign issues
976 post :bulk_edit, :ids => [1, 2], :notes => 'Bulk unassigning', :assigned_to_id => 'none'
981 post :bulk_edit, :ids => [1, 2], :notes => 'Bulk unassigning', :issue => {:assigned_to_id => 'none'}
977 assert_response 302
982 assert_response 302
978 # check that the issues were updated
983 # check that the issues were updated
979 assert_nil Issue.find(2).assigned_to
984 assert_nil Issue.find(2).assigned_to
980 end
985 end
981
986
982 def test_post_bulk_edit_should_allow_fixed_version_to_be_set_to_a_subproject
987 def test_post_bulk_edit_should_allow_fixed_version_to_be_set_to_a_subproject
983 @request.session[:user_id] = 2
988 @request.session[:user_id] = 2
984
989
985 post :bulk_edit,
990 post :bulk_edit, :ids => [1,2], :issue => {:fixed_version_id => 4}
986 :ids => [1,2],
987 :fixed_version_id => 4
988
991
989 assert_response :redirect
992 assert_response :redirect
990 issues = Issue.find([1,2])
993 issues = Issue.find([1,2])
991 issues.each do |issue|
994 issues.each do |issue|
992 assert_equal 4, issue.fixed_version_id
995 assert_equal 4, issue.fixed_version_id
993 assert_not_equal issue.project_id, issue.fixed_version.project_id
996 assert_not_equal issue.project_id, issue.fixed_version.project_id
994 end
997 end
995 end
998 end
996
999
997 def test_post_bulk_edit_should_redirect_back_using_the_back_url_parameter
1000 def test_post_bulk_edit_should_redirect_back_using_the_back_url_parameter
998 @request.session[:user_id] = 2
1001 @request.session[:user_id] = 2
999 post :bulk_edit, :ids => [1,2], :back_url => '/issues'
1002 post :bulk_edit, :ids => [1,2], :back_url => '/issues'
1000
1003
1001 assert_response :redirect
1004 assert_response :redirect
1002 assert_redirected_to '/issues'
1005 assert_redirected_to '/issues'
1003 end
1006 end
1004
1007
1005 def test_post_bulk_edit_should_not_redirect_back_using_the_back_url_parameter_off_the_host
1008 def test_post_bulk_edit_should_not_redirect_back_using_the_back_url_parameter_off_the_host
1006 @request.session[:user_id] = 2
1009 @request.session[:user_id] = 2
1007 post :bulk_edit, :ids => [1,2], :back_url => 'http://google.com'
1010 post :bulk_edit, :ids => [1,2], :back_url => 'http://google.com'
1008
1011
1009 assert_response :redirect
1012 assert_response :redirect
1010 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => Project.find(1).identifier
1013 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => Project.find(1).identifier
1011 end
1014 end
1012
1015
1013 def test_move_one_issue_to_another_project
1016 def test_move_one_issue_to_another_project
1014 @request.session[:user_id] = 2
1017 @request.session[:user_id] = 2
1015 post :move, :id => 1, :new_project_id => 2, :tracker_id => '', :assigned_to_id => '', :status_id => '', :start_date => '', :due_date => ''
1018 post :move, :id => 1, :new_project_id => 2, :tracker_id => '', :assigned_to_id => '', :status_id => '', :start_date => '', :due_date => ''
1016 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1019 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1017 assert_equal 2, Issue.find(1).project_id
1020 assert_equal 2, Issue.find(1).project_id
1018 end
1021 end
1019
1022
1020 def test_move_one_issue_to_another_project_should_follow_when_needed
1023 def test_move_one_issue_to_another_project_should_follow_when_needed
1021 @request.session[:user_id] = 2
1024 @request.session[:user_id] = 2
1022 post :move, :id => 1, :new_project_id => 2, :follow => '1'
1025 post :move, :id => 1, :new_project_id => 2, :follow => '1'
1023 assert_redirected_to '/issues/1'
1026 assert_redirected_to '/issues/1'
1024 end
1027 end
1025
1028
1026 def test_bulk_move_to_another_project
1029 def test_bulk_move_to_another_project
1027 @request.session[:user_id] = 2
1030 @request.session[:user_id] = 2
1028 post :move, :ids => [1, 2], :new_project_id => 2
1031 post :move, :ids => [1, 2], :new_project_id => 2
1029 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1032 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1030 # Issues moved to project 2
1033 # Issues moved to project 2
1031 assert_equal 2, Issue.find(1).project_id
1034 assert_equal 2, Issue.find(1).project_id
1032 assert_equal 2, Issue.find(2).project_id
1035 assert_equal 2, Issue.find(2).project_id
1033 # No tracker change
1036 # No tracker change
1034 assert_equal 1, Issue.find(1).tracker_id
1037 assert_equal 1, Issue.find(1).tracker_id
1035 assert_equal 2, Issue.find(2).tracker_id
1038 assert_equal 2, Issue.find(2).tracker_id
1036 end
1039 end
1037
1040
1038 def test_bulk_move_to_another_tracker
1041 def test_bulk_move_to_another_tracker
1039 @request.session[:user_id] = 2
1042 @request.session[:user_id] = 2
1040 post :move, :ids => [1, 2], :new_tracker_id => 2
1043 post :move, :ids => [1, 2], :new_tracker_id => 2
1041 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1044 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1042 assert_equal 2, Issue.find(1).tracker_id
1045 assert_equal 2, Issue.find(1).tracker_id
1043 assert_equal 2, Issue.find(2).tracker_id
1046 assert_equal 2, Issue.find(2).tracker_id
1044 end
1047 end
1045
1048
1046 def test_bulk_copy_to_another_project
1049 def test_bulk_copy_to_another_project
1047 @request.session[:user_id] = 2
1050 @request.session[:user_id] = 2
1048 assert_difference 'Issue.count', 2 do
1051 assert_difference 'Issue.count', 2 do
1049 assert_no_difference 'Project.find(1).issues.count' do
1052 assert_no_difference 'Project.find(1).issues.count' do
1050 post :move, :ids => [1, 2], :new_project_id => 2, :copy_options => {:copy => '1'}
1053 post :move, :ids => [1, 2], :new_project_id => 2, :copy_options => {:copy => '1'}
1051 end
1054 end
1052 end
1055 end
1053 assert_redirected_to 'projects/ecookbook/issues'
1056 assert_redirected_to 'projects/ecookbook/issues'
1054 end
1057 end
1055
1058
1056 context "#move via bulk copy" do
1059 context "#move via bulk copy" do
1057 should "allow not changing the issue's attributes" do
1060 should "allow not changing the issue's attributes" do
1058 @request.session[:user_id] = 2
1061 @request.session[:user_id] = 2
1059 issue_before_move = Issue.find(1)
1062 issue_before_move = Issue.find(1)
1060 assert_difference 'Issue.count', 1 do
1063 assert_difference 'Issue.count', 1 do
1061 assert_no_difference 'Project.find(1).issues.count' do
1064 assert_no_difference 'Project.find(1).issues.count' do
1062 post :move, :ids => [1], :new_project_id => 2, :copy_options => {:copy => '1'}, :new_tracker_id => '', :assigned_to_id => '', :status_id => '', :start_date => '', :due_date => ''
1065 post :move, :ids => [1], :new_project_id => 2, :copy_options => {:copy => '1'}, :new_tracker_id => '', :assigned_to_id => '', :status_id => '', :start_date => '', :due_date => ''
1063 end
1066 end
1064 end
1067 end
1065 issue_after_move = Issue.first(:order => 'id desc', :conditions => {:project_id => 2})
1068 issue_after_move = Issue.first(:order => 'id desc', :conditions => {:project_id => 2})
1066 assert_equal issue_before_move.tracker_id, issue_after_move.tracker_id
1069 assert_equal issue_before_move.tracker_id, issue_after_move.tracker_id
1067 assert_equal issue_before_move.status_id, issue_after_move.status_id
1070 assert_equal issue_before_move.status_id, issue_after_move.status_id
1068 assert_equal issue_before_move.assigned_to_id, issue_after_move.assigned_to_id
1071 assert_equal issue_before_move.assigned_to_id, issue_after_move.assigned_to_id
1069 end
1072 end
1070
1073
1071 should "allow changing the issue's attributes" do
1074 should "allow changing the issue's attributes" do
1072 @request.session[:user_id] = 2
1075 @request.session[:user_id] = 2
1073 assert_difference 'Issue.count', 2 do
1076 assert_difference 'Issue.count', 2 do
1074 assert_no_difference 'Project.find(1).issues.count' do
1077 assert_no_difference 'Project.find(1).issues.count' do
1075 post :move, :ids => [1, 2], :new_project_id => 2, :copy_options => {:copy => '1'}, :new_tracker_id => '', :assigned_to_id => 4, :status_id => 3, :start_date => '2009-12-01', :due_date => '2009-12-31'
1078 post :move, :ids => [1, 2], :new_project_id => 2, :copy_options => {:copy => '1'}, :new_tracker_id => '', :assigned_to_id => 4, :status_id => 3, :start_date => '2009-12-01', :due_date => '2009-12-31'
1076 end
1079 end
1077 end
1080 end
1078
1081
1079 copied_issues = Issue.all(:limit => 2, :order => 'id desc', :conditions => {:project_id => 2})
1082 copied_issues = Issue.all(:limit => 2, :order => 'id desc', :conditions => {:project_id => 2})
1080 assert_equal 2, copied_issues.size
1083 assert_equal 2, copied_issues.size
1081 copied_issues.each do |issue|
1084 copied_issues.each do |issue|
1082 assert_equal 2, issue.project_id, "Project is incorrect"
1085 assert_equal 2, issue.project_id, "Project is incorrect"
1083 assert_equal 4, issue.assigned_to_id, "Assigned to is incorrect"
1086 assert_equal 4, issue.assigned_to_id, "Assigned to is incorrect"
1084 assert_equal 3, issue.status_id, "Status is incorrect"
1087 assert_equal 3, issue.status_id, "Status is incorrect"
1085 assert_equal '2009-12-01', issue.start_date.to_s, "Start date is incorrect"
1088 assert_equal '2009-12-01', issue.start_date.to_s, "Start date is incorrect"
1086 assert_equal '2009-12-31', issue.due_date.to_s, "Due date is incorrect"
1089 assert_equal '2009-12-31', issue.due_date.to_s, "Due date is incorrect"
1087 end
1090 end
1088 end
1091 end
1089 end
1092 end
1090
1093
1091 def test_copy_to_another_project_should_follow_when_needed
1094 def test_copy_to_another_project_should_follow_when_needed
1092 @request.session[:user_id] = 2
1095 @request.session[:user_id] = 2
1093 post :move, :ids => [1], :new_project_id => 2, :copy_options => {:copy => '1'}, :follow => '1'
1096 post :move, :ids => [1], :new_project_id => 2, :copy_options => {:copy => '1'}, :follow => '1'
1094 issue = Issue.first(:order => 'id DESC')
1097 issue = Issue.first(:order => 'id DESC')
1095 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
1098 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
1096 end
1099 end
1097
1100
1098 def test_context_menu_one_issue
1101 def test_context_menu_one_issue
1099 @request.session[:user_id] = 2
1102 @request.session[:user_id] = 2
1100 get :context_menu, :ids => [1]
1103 get :context_menu, :ids => [1]
1101 assert_response :success
1104 assert_response :success
1102 assert_template 'context_menu'
1105 assert_template 'context_menu'
1103 assert_tag :tag => 'a', :content => 'Edit',
1106 assert_tag :tag => 'a', :content => 'Edit',
1104 :attributes => { :href => '/issues/1/edit',
1107 :attributes => { :href => '/issues/1/edit',
1105 :class => 'icon-edit' }
1108 :class => 'icon-edit' }
1106 assert_tag :tag => 'a', :content => 'Closed',
1109 assert_tag :tag => 'a', :content => 'Closed',
1107 :attributes => { :href => '/issues/1/edit?issue%5Bstatus_id%5D=5',
1110 :attributes => { :href => '/issues/1/edit?issue%5Bstatus_id%5D=5',
1108 :class => '' }
1111 :class => '' }
1109 assert_tag :tag => 'a', :content => 'Immediate',
1112 assert_tag :tag => 'a', :content => 'Immediate',
1110 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;priority_id=8',
1113 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;priority_id=8',
1111 :class => '' }
1114 :class => '' }
1112 # Versions
1115 # Versions
1113 assert_tag :tag => 'a', :content => '2.0',
1116 assert_tag :tag => 'a', :content => '2.0',
1114 :attributes => { :href => '/issues/bulk_edit?fixed_version_id=3&amp;ids%5B%5D=1',
1117 :attributes => { :href => '/issues/bulk_edit?fixed_version_id=3&amp;ids%5B%5D=1',
1115 :class => '' }
1118 :class => '' }
1116 assert_tag :tag => 'a', :content => 'eCookbook Subproject 1 - 2.0',
1119 assert_tag :tag => 'a', :content => 'eCookbook Subproject 1 - 2.0',
1117 :attributes => { :href => '/issues/bulk_edit?fixed_version_id=4&amp;ids%5B%5D=1',
1120 :attributes => { :href => '/issues/bulk_edit?fixed_version_id=4&amp;ids%5B%5D=1',
1118 :class => '' }
1121 :class => '' }
1119
1122
1120 assert_tag :tag => 'a', :content => 'Dave Lopper',
1123 assert_tag :tag => 'a', :content => 'Dave Lopper',
1121 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1',
1124 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1',
1122 :class => '' }
1125 :class => '' }
1123 assert_tag :tag => 'a', :content => 'Duplicate',
1126 assert_tag :tag => 'a', :content => 'Duplicate',
1124 :attributes => { :href => '/projects/ecookbook/issues/1/copy',
1127 :attributes => { :href => '/projects/ecookbook/issues/1/copy',
1125 :class => 'icon-duplicate' }
1128 :class => 'icon-duplicate' }
1126 assert_tag :tag => 'a', :content => 'Copy',
1129 assert_tag :tag => 'a', :content => 'Copy',
1127 :attributes => { :href => '/issues/move?copy_options%5Bcopy%5D=t&amp;ids%5B%5D=1',
1130 :attributes => { :href => '/issues/move?copy_options%5Bcopy%5D=t&amp;ids%5B%5D=1',
1128 :class => 'icon-copy' }
1131 :class => 'icon-copy' }
1129 assert_tag :tag => 'a', :content => 'Move',
1132 assert_tag :tag => 'a', :content => 'Move',
1130 :attributes => { :href => '/issues/move?ids%5B%5D=1',
1133 :attributes => { :href => '/issues/move?ids%5B%5D=1',
1131 :class => 'icon-move' }
1134 :class => 'icon-move' }
1132 assert_tag :tag => 'a', :content => 'Delete',
1135 assert_tag :tag => 'a', :content => 'Delete',
1133 :attributes => { :href => '/issues/destroy?ids%5B%5D=1',
1136 :attributes => { :href => '/issues/destroy?ids%5B%5D=1',
1134 :class => 'icon-del' }
1137 :class => 'icon-del' }
1135 end
1138 end
1136
1139
1137 def test_context_menu_one_issue_by_anonymous
1140 def test_context_menu_one_issue_by_anonymous
1138 get :context_menu, :ids => [1]
1141 get :context_menu, :ids => [1]
1139 assert_response :success
1142 assert_response :success
1140 assert_template 'context_menu'
1143 assert_template 'context_menu'
1141 assert_tag :tag => 'a', :content => 'Delete',
1144 assert_tag :tag => 'a', :content => 'Delete',
1142 :attributes => { :href => '#',
1145 :attributes => { :href => '#',
1143 :class => 'icon-del disabled' }
1146 :class => 'icon-del disabled' }
1144 end
1147 end
1145
1148
1146 def test_context_menu_multiple_issues_of_same_project
1149 def test_context_menu_multiple_issues_of_same_project
1147 @request.session[:user_id] = 2
1150 @request.session[:user_id] = 2
1148 get :context_menu, :ids => [1, 2]
1151 get :context_menu, :ids => [1, 2]
1149 assert_response :success
1152 assert_response :success
1150 assert_template 'context_menu'
1153 assert_template 'context_menu'
1151 assert_tag :tag => 'a', :content => 'Edit',
1154 assert_tag :tag => 'a', :content => 'Edit',
1152 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2',
1155 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2',
1153 :class => 'icon-edit' }
1156 :class => 'icon-edit' }
1154 assert_tag :tag => 'a', :content => 'Immediate',
1157 assert_tag :tag => 'a', :content => 'Immediate',
1155 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2&amp;priority_id=8',
1158 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2&amp;priority_id=8',
1156 :class => '' }
1159 :class => '' }
1157 assert_tag :tag => 'a', :content => 'Dave Lopper',
1160 assert_tag :tag => 'a', :content => 'Dave Lopper',
1158 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1&amp;ids%5B%5D=2',
1161 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1&amp;ids%5B%5D=2',
1159 :class => '' }
1162 :class => '' }
1160 assert_tag :tag => 'a', :content => 'Copy',
1163 assert_tag :tag => 'a', :content => 'Copy',
1161 :attributes => { :href => '/issues/move?copy_options%5Bcopy%5D=t&amp;ids%5B%5D=1&amp;ids%5B%5D=2',
1164 :attributes => { :href => '/issues/move?copy_options%5Bcopy%5D=t&amp;ids%5B%5D=1&amp;ids%5B%5D=2',
1162 :class => 'icon-copy' }
1165 :class => 'icon-copy' }
1163 assert_tag :tag => 'a', :content => 'Move',
1166 assert_tag :tag => 'a', :content => 'Move',
1164 :attributes => { :href => '/issues/move?ids%5B%5D=1&amp;ids%5B%5D=2',
1167 :attributes => { :href => '/issues/move?ids%5B%5D=1&amp;ids%5B%5D=2',
1165 :class => 'icon-move' }
1168 :class => 'icon-move' }
1166 assert_tag :tag => 'a', :content => 'Delete',
1169 assert_tag :tag => 'a', :content => 'Delete',
1167 :attributes => { :href => '/issues/destroy?ids%5B%5D=1&amp;ids%5B%5D=2',
1170 :attributes => { :href => '/issues/destroy?ids%5B%5D=1&amp;ids%5B%5D=2',
1168 :class => 'icon-del' }
1171 :class => 'icon-del' }
1169 end
1172 end
1170
1173
1171 def test_context_menu_multiple_issues_of_different_project
1174 def test_context_menu_multiple_issues_of_different_project
1172 @request.session[:user_id] = 2
1175 @request.session[:user_id] = 2
1173 get :context_menu, :ids => [1, 2, 4]
1176 get :context_menu, :ids => [1, 2, 4]
1174 assert_response :success
1177 assert_response :success
1175 assert_template 'context_menu'
1178 assert_template 'context_menu'
1176 assert_tag :tag => 'a', :content => 'Delete',
1179 assert_tag :tag => 'a', :content => 'Delete',
1177 :attributes => { :href => '#',
1180 :attributes => { :href => '#',
1178 :class => 'icon-del disabled' }
1181 :class => 'icon-del disabled' }
1179 end
1182 end
1180
1183
1181 def test_destroy_issue_with_no_time_entries
1184 def test_destroy_issue_with_no_time_entries
1182 assert_nil TimeEntry.find_by_issue_id(2)
1185 assert_nil TimeEntry.find_by_issue_id(2)
1183 @request.session[:user_id] = 2
1186 @request.session[:user_id] = 2
1184 post :destroy, :id => 2
1187 post :destroy, :id => 2
1185 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1188 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1186 assert_nil Issue.find_by_id(2)
1189 assert_nil Issue.find_by_id(2)
1187 end
1190 end
1188
1191
1189 def test_destroy_issues_with_time_entries
1192 def test_destroy_issues_with_time_entries
1190 @request.session[:user_id] = 2
1193 @request.session[:user_id] = 2
1191 post :destroy, :ids => [1, 3]
1194 post :destroy, :ids => [1, 3]
1192 assert_response :success
1195 assert_response :success
1193 assert_template 'destroy'
1196 assert_template 'destroy'
1194 assert_not_nil assigns(:hours)
1197 assert_not_nil assigns(:hours)
1195 assert Issue.find_by_id(1) && Issue.find_by_id(3)
1198 assert Issue.find_by_id(1) && Issue.find_by_id(3)
1196 end
1199 end
1197
1200
1198 def test_destroy_issues_and_destroy_time_entries
1201 def test_destroy_issues_and_destroy_time_entries
1199 @request.session[:user_id] = 2
1202 @request.session[:user_id] = 2
1200 post :destroy, :ids => [1, 3], :todo => 'destroy'
1203 post :destroy, :ids => [1, 3], :todo => 'destroy'
1201 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1204 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1202 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1205 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1203 assert_nil TimeEntry.find_by_id([1, 2])
1206 assert_nil TimeEntry.find_by_id([1, 2])
1204 end
1207 end
1205
1208
1206 def test_destroy_issues_and_assign_time_entries_to_project
1209 def test_destroy_issues_and_assign_time_entries_to_project
1207 @request.session[:user_id] = 2
1210 @request.session[:user_id] = 2
1208 post :destroy, :ids => [1, 3], :todo => 'nullify'
1211 post :destroy, :ids => [1, 3], :todo => 'nullify'
1209 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1212 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1210 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1213 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1211 assert_nil TimeEntry.find(1).issue_id
1214 assert_nil TimeEntry.find(1).issue_id
1212 assert_nil TimeEntry.find(2).issue_id
1215 assert_nil TimeEntry.find(2).issue_id
1213 end
1216 end
1214
1217
1215 def test_destroy_issues_and_reassign_time_entries_to_another_issue
1218 def test_destroy_issues_and_reassign_time_entries_to_another_issue
1216 @request.session[:user_id] = 2
1219 @request.session[:user_id] = 2
1217 post :destroy, :ids => [1, 3], :todo => 'reassign', :reassign_to_id => 2
1220 post :destroy, :ids => [1, 3], :todo => 'reassign', :reassign_to_id => 2
1218 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1221 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1219 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1222 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1220 assert_equal 2, TimeEntry.find(1).issue_id
1223 assert_equal 2, TimeEntry.find(1).issue_id
1221 assert_equal 2, TimeEntry.find(2).issue_id
1224 assert_equal 2, TimeEntry.find(2).issue_id
1222 end
1225 end
1223
1226
1224 def test_default_search_scope
1227 def test_default_search_scope
1225 get :index
1228 get :index
1226 assert_tag :div, :attributes => {:id => 'quick-search'},
1229 assert_tag :div, :attributes => {:id => 'quick-search'},
1227 :child => {:tag => 'form',
1230 :child => {:tag => 'form',
1228 :child => {:tag => 'input', :attributes => {:name => 'issues', :type => 'hidden', :value => '1'}}}
1231 :child => {:tag => 'input', :attributes => {:name => 'issues', :type => 'hidden', :value => '1'}}}
1229 end
1232 end
1230 end
1233 end
General Comments 0
You need to be logged in to leave comments. Login now