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