##// END OF EJS Templates
Adds ability to bulk copy issues (#1847)....
Jean-Philippe Lang -
r2311:1ad255155949
parent child
Show More
@@ -1,489 +1,489
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
20
21 before_filter :find_issue, :only => [:show, :edit, :reply]
21 before_filter :find_issue, :only => [:show, :edit, :reply]
22 before_filter :find_issues, :only => [:bulk_edit, :move, :destroy]
22 before_filter :find_issues, :only => [:bulk_edit, :move, :destroy]
23 before_filter :find_project, :only => [:new, :update_form, :preview]
23 before_filter :find_project, :only => [:new, :update_form, :preview]
24 before_filter :authorize, :except => [:index, :changes, :gantt, :calendar, :preview, :update_form, :context_menu]
24 before_filter :authorize, :except => [:index, :changes, :gantt, :calendar, :preview, :update_form, :context_menu]
25 before_filter :find_optional_project, :only => [:index, :changes, :gantt, :calendar]
25 before_filter :find_optional_project, :only => [:index, :changes, :gantt, :calendar]
26 accept_key_auth :index, :changes
26 accept_key_auth :index, :changes
27
27
28 helper :journals
28 helper :journals
29 helper :projects
29 helper :projects
30 include ProjectsHelper
30 include ProjectsHelper
31 helper :custom_fields
31 helper :custom_fields
32 include CustomFieldsHelper
32 include CustomFieldsHelper
33 helper :issue_relations
33 helper :issue_relations
34 include IssueRelationsHelper
34 include IssueRelationsHelper
35 helper :watchers
35 helper :watchers
36 include WatchersHelper
36 include WatchersHelper
37 helper :attachments
37 helper :attachments
38 include AttachmentsHelper
38 include AttachmentsHelper
39 helper :queries
39 helper :queries
40 helper :sort
40 helper :sort
41 include SortHelper
41 include SortHelper
42 include IssuesHelper
42 include IssuesHelper
43 helper :timelog
43 helper :timelog
44 include Redmine::Export::PDF
44 include Redmine::Export::PDF
45
45
46 def index
46 def index
47 retrieve_query
47 retrieve_query
48 sort_init 'id', 'desc'
48 sort_init 'id', 'desc'
49 sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
49 sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
50
50
51 if @query.valid?
51 if @query.valid?
52 limit = per_page_option
52 limit = per_page_option
53 respond_to do |format|
53 respond_to do |format|
54 format.html { }
54 format.html { }
55 format.atom { }
55 format.atom { }
56 format.csv { limit = Setting.issues_export_limit.to_i }
56 format.csv { limit = Setting.issues_export_limit.to_i }
57 format.pdf { limit = Setting.issues_export_limit.to_i }
57 format.pdf { limit = Setting.issues_export_limit.to_i }
58 end
58 end
59 @issue_count = Issue.count(:include => [:status, :project], :conditions => @query.statement)
59 @issue_count = Issue.count(:include => [:status, :project], :conditions => @query.statement)
60 @issue_pages = Paginator.new self, @issue_count, limit, params['page']
60 @issue_pages = Paginator.new self, @issue_count, limit, params['page']
61 @issues = Issue.find :all, :order => sort_clause,
61 @issues = Issue.find :all, :order => sort_clause,
62 :include => [ :assigned_to, :status, :tracker, :project, :priority, :category, :fixed_version ],
62 :include => [ :assigned_to, :status, :tracker, :project, :priority, :category, :fixed_version ],
63 :conditions => @query.statement,
63 :conditions => @query.statement,
64 :limit => limit,
64 :limit => limit,
65 :offset => @issue_pages.current.offset
65 :offset => @issue_pages.current.offset
66 respond_to do |format|
66 respond_to do |format|
67 format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
67 format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
68 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
68 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
69 format.csv { send_data(issues_to_csv(@issues, @project).read, :type => 'text/csv; header=present', :filename => 'export.csv') }
69 format.csv { send_data(issues_to_csv(@issues, @project).read, :type => 'text/csv; header=present', :filename => 'export.csv') }
70 format.pdf { send_data(issues_to_pdf(@issues, @project), :type => 'application/pdf', :filename => 'export.pdf') }
70 format.pdf { send_data(issues_to_pdf(@issues, @project), :type => 'application/pdf', :filename => 'export.pdf') }
71 end
71 end
72 else
72 else
73 # Send html if the query is not valid
73 # Send html if the query is not valid
74 render(:template => 'issues/index.rhtml', :layout => !request.xhr?)
74 render(:template => 'issues/index.rhtml', :layout => !request.xhr?)
75 end
75 end
76 rescue ActiveRecord::RecordNotFound
76 rescue ActiveRecord::RecordNotFound
77 render_404
77 render_404
78 end
78 end
79
79
80 def changes
80 def changes
81 retrieve_query
81 retrieve_query
82 sort_init 'id', 'desc'
82 sort_init 'id', 'desc'
83 sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
83 sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
84
84
85 if @query.valid?
85 if @query.valid?
86 @journals = Journal.find :all, :include => [ :details, :user, {:issue => [:project, :author, :tracker, :status]} ],
86 @journals = Journal.find :all, :include => [ :details, :user, {:issue => [:project, :author, :tracker, :status]} ],
87 :conditions => @query.statement,
87 :conditions => @query.statement,
88 :limit => 25,
88 :limit => 25,
89 :order => "#{Journal.table_name}.created_on DESC"
89 :order => "#{Journal.table_name}.created_on DESC"
90 end
90 end
91 @title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
91 @title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
92 render :layout => false, :content_type => 'application/atom+xml'
92 render :layout => false, :content_type => 'application/atom+xml'
93 rescue ActiveRecord::RecordNotFound
93 rescue ActiveRecord::RecordNotFound
94 render_404
94 render_404
95 end
95 end
96
96
97 def show
97 def show
98 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
98 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
99 @journals.each_with_index {|j,i| j.indice = i+1}
99 @journals.each_with_index {|j,i| j.indice = i+1}
100 @journals.reverse! if User.current.wants_comments_in_reverse_order?
100 @journals.reverse! if User.current.wants_comments_in_reverse_order?
101 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
101 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
102 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
102 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
103 @priorities = Enumeration::get_values('IPRI')
103 @priorities = Enumeration::get_values('IPRI')
104 @time_entry = TimeEntry.new
104 @time_entry = TimeEntry.new
105 respond_to do |format|
105 respond_to do |format|
106 format.html { render :template => 'issues/show.rhtml' }
106 format.html { render :template => 'issues/show.rhtml' }
107 format.atom { render :action => 'changes', :layout => false, :content_type => 'application/atom+xml' }
107 format.atom { render :action => 'changes', :layout => false, :content_type => 'application/atom+xml' }
108 format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
108 format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
109 end
109 end
110 end
110 end
111
111
112 # Add a new issue
112 # Add a new issue
113 # The new issue will be created from an existing one if copy_from parameter is given
113 # The new issue will be created from an existing one if copy_from parameter is given
114 def new
114 def new
115 @issue = Issue.new
115 @issue = Issue.new
116 @issue.copy_from(params[:copy_from]) if params[:copy_from]
116 @issue.copy_from(params[:copy_from]) if params[:copy_from]
117 @issue.project = @project
117 @issue.project = @project
118 # Tracker must be set before custom field values
118 # Tracker must be set before custom field values
119 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
119 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
120 if @issue.tracker.nil?
120 if @issue.tracker.nil?
121 flash.now[:error] = 'No tracker is associated to this project. Please check the Project settings.'
121 flash.now[:error] = 'No tracker is associated to this project. Please check the Project settings.'
122 render :nothing => true, :layout => true
122 render :nothing => true, :layout => true
123 return
123 return
124 end
124 end
125 if params[:issue].is_a?(Hash)
125 if params[:issue].is_a?(Hash)
126 @issue.attributes = params[:issue]
126 @issue.attributes = params[:issue]
127 @issue.watcher_user_ids = params[:issue]['watcher_user_ids'] if User.current.allowed_to?(:add_issue_watchers, @project)
127 @issue.watcher_user_ids = params[:issue]['watcher_user_ids'] if User.current.allowed_to?(:add_issue_watchers, @project)
128 end
128 end
129 @issue.author = User.current
129 @issue.author = User.current
130
130
131 default_status = IssueStatus.default
131 default_status = IssueStatus.default
132 unless default_status
132 unless default_status
133 flash.now[:error] = 'No default issue status is defined. Please check your configuration (Go to "Administration -> Issue statuses").'
133 flash.now[:error] = 'No default issue status is defined. Please check your configuration (Go to "Administration -> Issue statuses").'
134 render :nothing => true, :layout => true
134 render :nothing => true, :layout => true
135 return
135 return
136 end
136 end
137 @issue.status = default_status
137 @issue.status = default_status
138 @allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(User.current.role_for_project(@project), @issue.tracker)).uniq
138 @allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(User.current.role_for_project(@project), @issue.tracker)).uniq
139
139
140 if request.get? || request.xhr?
140 if request.get? || request.xhr?
141 @issue.start_date ||= Date.today
141 @issue.start_date ||= Date.today
142 else
142 else
143 requested_status = IssueStatus.find_by_id(params[:issue][:status_id])
143 requested_status = IssueStatus.find_by_id(params[:issue][:status_id])
144 # Check that the user is allowed to apply the requested status
144 # Check that the user is allowed to apply the requested status
145 @issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status
145 @issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status
146 if @issue.save
146 if @issue.save
147 attach_files(@issue, params[:attachments])
147 attach_files(@issue, params[:attachments])
148 flash[:notice] = l(:notice_successful_create)
148 flash[:notice] = l(:notice_successful_create)
149 Mailer.deliver_issue_add(@issue) if Setting.notified_events.include?('issue_added')
149 Mailer.deliver_issue_add(@issue) if Setting.notified_events.include?('issue_added')
150 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
150 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
151 redirect_to(params[:continue] ? { :action => 'new', :tracker_id => @issue.tracker } :
151 redirect_to(params[:continue] ? { :action => 'new', :tracker_id => @issue.tracker } :
152 { :action => 'show', :id => @issue })
152 { :action => 'show', :id => @issue })
153 return
153 return
154 end
154 end
155 end
155 end
156 @priorities = Enumeration::get_values('IPRI')
156 @priorities = Enumeration::get_values('IPRI')
157 render :layout => !request.xhr?
157 render :layout => !request.xhr?
158 end
158 end
159
159
160 # Attributes that can be updated on workflow transition (without :edit permission)
160 # Attributes that can be updated on workflow transition (without :edit permission)
161 # TODO: make it configurable (at least per role)
161 # TODO: make it configurable (at least per role)
162 UPDATABLE_ATTRS_ON_TRANSITION = %w(status_id assigned_to_id fixed_version_id done_ratio) unless const_defined?(:UPDATABLE_ATTRS_ON_TRANSITION)
162 UPDATABLE_ATTRS_ON_TRANSITION = %w(status_id assigned_to_id fixed_version_id done_ratio) unless const_defined?(:UPDATABLE_ATTRS_ON_TRANSITION)
163
163
164 def edit
164 def edit
165 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
165 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
166 @priorities = Enumeration::get_values('IPRI')
166 @priorities = Enumeration::get_values('IPRI')
167 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
167 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
168 @time_entry = TimeEntry.new
168 @time_entry = TimeEntry.new
169
169
170 @notes = params[:notes]
170 @notes = params[:notes]
171 journal = @issue.init_journal(User.current, @notes)
171 journal = @issue.init_journal(User.current, @notes)
172 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
172 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
173 if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue]
173 if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue]
174 attrs = params[:issue].dup
174 attrs = params[:issue].dup
175 attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed
175 attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed
176 attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s}
176 attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s}
177 @issue.attributes = attrs
177 @issue.attributes = attrs
178 end
178 end
179
179
180 if request.post?
180 if request.post?
181 @time_entry = TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
181 @time_entry = TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
182 @time_entry.attributes = params[:time_entry]
182 @time_entry.attributes = params[:time_entry]
183 attachments = attach_files(@issue, params[:attachments])
183 attachments = attach_files(@issue, params[:attachments])
184 attachments.each {|a| journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
184 attachments.each {|a| journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
185
185
186 call_hook(:controller_issues_edit_before_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
186 call_hook(:controller_issues_edit_before_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
187
187
188 if (@time_entry.hours.nil? || @time_entry.valid?) && @issue.save
188 if (@time_entry.hours.nil? || @time_entry.valid?) && @issue.save
189 # Log spend time
189 # Log spend time
190 if current_role.allowed_to?(:log_time)
190 if current_role.allowed_to?(:log_time)
191 @time_entry.save
191 @time_entry.save
192 end
192 end
193 if !journal.new_record?
193 if !journal.new_record?
194 # Only send notification if something was actually changed
194 # Only send notification if something was actually changed
195 flash[:notice] = l(:notice_successful_update)
195 flash[:notice] = l(:notice_successful_update)
196 Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
196 Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
197 end
197 end
198 call_hook(:controller_issues_edit_after_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
198 call_hook(:controller_issues_edit_after_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
199 redirect_to(params[:back_to] || {:action => 'show', :id => @issue})
199 redirect_to(params[:back_to] || {:action => 'show', :id => @issue})
200 end
200 end
201 end
201 end
202 rescue ActiveRecord::StaleObjectError
202 rescue ActiveRecord::StaleObjectError
203 # Optimistic locking exception
203 # Optimistic locking exception
204 flash.now[:error] = l(:notice_locking_conflict)
204 flash.now[:error] = l(:notice_locking_conflict)
205 end
205 end
206
206
207 def reply
207 def reply
208 journal = Journal.find(params[:journal_id]) if params[:journal_id]
208 journal = Journal.find(params[:journal_id]) if params[:journal_id]
209 if journal
209 if journal
210 user = journal.user
210 user = journal.user
211 text = journal.notes
211 text = journal.notes
212 else
212 else
213 user = @issue.author
213 user = @issue.author
214 text = @issue.description
214 text = @issue.description
215 end
215 end
216 content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
216 content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
217 content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
217 content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
218 render(:update) { |page|
218 render(:update) { |page|
219 page.<< "$('notes').value = \"#{content}\";"
219 page.<< "$('notes').value = \"#{content}\";"
220 page.show 'update'
220 page.show 'update'
221 page << "Form.Element.focus('notes');"
221 page << "Form.Element.focus('notes');"
222 page << "Element.scrollTo('update');"
222 page << "Element.scrollTo('update');"
223 page << "$('notes').scrollTop = $('notes').scrollHeight - $('notes').clientHeight;"
223 page << "$('notes').scrollTop = $('notes').scrollHeight - $('notes').clientHeight;"
224 }
224 }
225 end
225 end
226
226
227 # Bulk edit a set of issues
227 # Bulk edit a set of issues
228 def bulk_edit
228 def bulk_edit
229 if request.post?
229 if request.post?
230 status = params[:status_id].blank? ? nil : IssueStatus.find_by_id(params[:status_id])
230 status = params[:status_id].blank? ? nil : IssueStatus.find_by_id(params[:status_id])
231 priority = params[:priority_id].blank? ? nil : Enumeration.find_by_id(params[:priority_id])
231 priority = params[:priority_id].blank? ? nil : Enumeration.find_by_id(params[:priority_id])
232 assigned_to = (params[:assigned_to_id].blank? || params[:assigned_to_id] == 'none') ? nil : User.find_by_id(params[:assigned_to_id])
232 assigned_to = (params[:assigned_to_id].blank? || params[:assigned_to_id] == 'none') ? nil : User.find_by_id(params[:assigned_to_id])
233 category = (params[:category_id].blank? || params[:category_id] == 'none') ? nil : @project.issue_categories.find_by_id(params[:category_id])
233 category = (params[:category_id].blank? || params[:category_id] == 'none') ? nil : @project.issue_categories.find_by_id(params[:category_id])
234 fixed_version = (params[:fixed_version_id].blank? || params[:fixed_version_id] == 'none') ? nil : @project.versions.find_by_id(params[:fixed_version_id])
234 fixed_version = (params[:fixed_version_id].blank? || params[:fixed_version_id] == 'none') ? nil : @project.versions.find_by_id(params[:fixed_version_id])
235
235
236 unsaved_issue_ids = []
236 unsaved_issue_ids = []
237 @issues.each do |issue|
237 @issues.each do |issue|
238 journal = issue.init_journal(User.current, params[:notes])
238 journal = issue.init_journal(User.current, params[:notes])
239 issue.priority = priority if priority
239 issue.priority = priority if priority
240 issue.assigned_to = assigned_to if assigned_to || params[:assigned_to_id] == 'none'
240 issue.assigned_to = assigned_to if assigned_to || params[:assigned_to_id] == 'none'
241 issue.category = category if category || params[:category_id] == 'none'
241 issue.category = category if category || params[:category_id] == 'none'
242 issue.fixed_version = fixed_version if fixed_version || params[:fixed_version_id] == 'none'
242 issue.fixed_version = fixed_version if fixed_version || params[:fixed_version_id] == 'none'
243 issue.start_date = params[:start_date] unless params[:start_date].blank?
243 issue.start_date = params[:start_date] unless params[:start_date].blank?
244 issue.due_date = params[:due_date] unless params[:due_date].blank?
244 issue.due_date = params[:due_date] unless params[:due_date].blank?
245 issue.done_ratio = params[:done_ratio] unless params[:done_ratio].blank?
245 issue.done_ratio = params[:done_ratio] unless params[:done_ratio].blank?
246 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
246 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
247 # Don't save any change to the issue if the user is not authorized to apply the requested status
247 # Don't save any change to the issue if the user is not authorized to apply the requested status
248 if (status.nil? || (issue.status.new_status_allowed_to?(status, current_role, issue.tracker) && issue.status = status)) && issue.save
248 if (status.nil? || (issue.status.new_status_allowed_to?(status, current_role, issue.tracker) && issue.status = status)) && issue.save
249 # Send notification for each issue (if changed)
249 # Send notification for each issue (if changed)
250 Mailer.deliver_issue_edit(journal) if journal.details.any? && Setting.notified_events.include?('issue_updated')
250 Mailer.deliver_issue_edit(journal) if journal.details.any? && Setting.notified_events.include?('issue_updated')
251 else
251 else
252 # Keep unsaved issue ids to display them in flash error
252 # Keep unsaved issue ids to display them in flash error
253 unsaved_issue_ids << issue.id
253 unsaved_issue_ids << issue.id
254 end
254 end
255 end
255 end
256 if unsaved_issue_ids.empty?
256 if unsaved_issue_ids.empty?
257 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
257 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
258 else
258 else
259 flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #'))
259 flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #'))
260 end
260 end
261 redirect_to(params[:back_to] || {:controller => 'issues', :action => 'index', :project_id => @project})
261 redirect_to(params[:back_to] || {:controller => 'issues', :action => 'index', :project_id => @project})
262 return
262 return
263 end
263 end
264 # Find potential statuses the user could be allowed to switch issues to
264 # Find potential statuses the user could be allowed to switch issues to
265 @available_statuses = Workflow.find(:all, :include => :new_status,
265 @available_statuses = Workflow.find(:all, :include => :new_status,
266 :conditions => {:role_id => current_role.id}).collect(&:new_status).compact.uniq.sort
266 :conditions => {:role_id => current_role.id}).collect(&:new_status).compact.uniq.sort
267 end
267 end
268
268
269 def move
269 def move
270 @allowed_projects = []
270 @allowed_projects = []
271 # find projects to which the user is allowed to move the issue
271 # find projects to which the user is allowed to move the issue
272 if User.current.admin?
272 if User.current.admin?
273 # admin is allowed to move issues to any active (visible) project
273 # admin is allowed to move issues to any active (visible) project
274 @allowed_projects = Project.find(:all, :conditions => Project.visible_by(User.current), :order => 'name')
274 @allowed_projects = Project.find(:all, :conditions => Project.visible_by(User.current), :order => 'name')
275 else
275 else
276 User.current.memberships.each {|m| @allowed_projects << m.project if m.role.allowed_to?(:move_issues)}
276 User.current.memberships.each {|m| @allowed_projects << m.project if m.role.allowed_to?(:move_issues)}
277 end
277 end
278 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id]
278 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id]
279 @target_project ||= @project
279 @target_project ||= @project
280 @trackers = @target_project.trackers
280 @trackers = @target_project.trackers
281 if request.post?
281 if request.post?
282 new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id])
282 new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id])
283 unsaved_issue_ids = []
283 unsaved_issue_ids = []
284 @issues.each do |issue|
284 @issues.each do |issue|
285 issue.init_journal(User.current)
285 issue.init_journal(User.current)
286 unsaved_issue_ids << issue.id unless issue.move_to(@target_project, new_tracker)
286 unsaved_issue_ids << issue.id unless issue.move_to(@target_project, new_tracker, params[:copy_options])
287 end
287 end
288 if unsaved_issue_ids.empty?
288 if unsaved_issue_ids.empty?
289 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
289 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
290 else
290 else
291 flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #'))
291 flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #'))
292 end
292 end
293 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
293 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
294 return
294 return
295 end
295 end
296 render :layout => false if request.xhr?
296 render :layout => false if request.xhr?
297 end
297 end
298
298
299 def destroy
299 def destroy
300 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
300 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
301 if @hours > 0
301 if @hours > 0
302 case params[:todo]
302 case params[:todo]
303 when 'destroy'
303 when 'destroy'
304 # nothing to do
304 # nothing to do
305 when 'nullify'
305 when 'nullify'
306 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
306 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
307 when 'reassign'
307 when 'reassign'
308 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
308 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
309 if reassign_to.nil?
309 if reassign_to.nil?
310 flash.now[:error] = l(:error_issue_not_found_in_project)
310 flash.now[:error] = l(:error_issue_not_found_in_project)
311 return
311 return
312 else
312 else
313 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
313 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
314 end
314 end
315 else
315 else
316 # display the destroy form
316 # display the destroy form
317 return
317 return
318 end
318 end
319 end
319 end
320 @issues.each(&:destroy)
320 @issues.each(&:destroy)
321 redirect_to :action => 'index', :project_id => @project
321 redirect_to :action => 'index', :project_id => @project
322 end
322 end
323
323
324 def gantt
324 def gantt
325 @gantt = Redmine::Helpers::Gantt.new(params)
325 @gantt = Redmine::Helpers::Gantt.new(params)
326 retrieve_query
326 retrieve_query
327 if @query.valid?
327 if @query.valid?
328 events = []
328 events = []
329 # Issues that have start and due dates
329 # Issues that have start and due dates
330 events += Issue.find(:all,
330 events += Issue.find(:all,
331 :order => "start_date, due_date",
331 :order => "start_date, due_date",
332 :include => [:tracker, :status, :assigned_to, :priority, :project],
332 :include => [:tracker, :status, :assigned_to, :priority, :project],
333 :conditions => ["(#{@query.statement}) AND (((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]
333 :conditions => ["(#{@query.statement}) AND (((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]
334 )
334 )
335 # Issues that don't have a due date but that are assigned to a version with a date
335 # Issues that don't have a due date but that are assigned to a version with a date
336 events += Issue.find(:all,
336 events += Issue.find(:all,
337 :order => "start_date, effective_date",
337 :order => "start_date, effective_date",
338 :include => [:tracker, :status, :assigned_to, :priority, :project, :fixed_version],
338 :include => [:tracker, :status, :assigned_to, :priority, :project, :fixed_version],
339 :conditions => ["(#{@query.statement}) AND (((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]
339 :conditions => ["(#{@query.statement}) AND (((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]
340 )
340 )
341 # Versions
341 # Versions
342 events += Version.find(:all, :include => :project,
342 events += Version.find(:all, :include => :project,
343 :conditions => ["(#{@query.project_statement}) AND effective_date BETWEEN ? AND ?", @gantt.date_from, @gantt.date_to])
343 :conditions => ["(#{@query.project_statement}) AND effective_date BETWEEN ? AND ?", @gantt.date_from, @gantt.date_to])
344
344
345 @gantt.events = events
345 @gantt.events = events
346 end
346 end
347
347
348 respond_to do |format|
348 respond_to do |format|
349 format.html { render :template => "issues/gantt.rhtml", :layout => !request.xhr? }
349 format.html { render :template => "issues/gantt.rhtml", :layout => !request.xhr? }
350 format.png { send_data(@gantt.to_image, :disposition => 'inline', :type => 'image/png', :filename => "#{@project.identifier}-gantt.png") } if @gantt.respond_to?('to_image')
350 format.png { send_data(@gantt.to_image, :disposition => 'inline', :type => 'image/png', :filename => "#{@project.identifier}-gantt.png") } if @gantt.respond_to?('to_image')
351 format.pdf { send_data(gantt_to_pdf(@gantt, @project), :type => 'application/pdf', :filename => "#{@project.nil? ? '' : "#{@project.identifier}-" }gantt.pdf") }
351 format.pdf { send_data(gantt_to_pdf(@gantt, @project), :type => 'application/pdf', :filename => "#{@project.nil? ? '' : "#{@project.identifier}-" }gantt.pdf") }
352 end
352 end
353 end
353 end
354
354
355 def calendar
355 def calendar
356 if params[:year] and params[:year].to_i > 1900
356 if params[:year] and params[:year].to_i > 1900
357 @year = params[:year].to_i
357 @year = params[:year].to_i
358 if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
358 if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
359 @month = params[:month].to_i
359 @month = params[:month].to_i
360 end
360 end
361 end
361 end
362 @year ||= Date.today.year
362 @year ||= Date.today.year
363 @month ||= Date.today.month
363 @month ||= Date.today.month
364
364
365 @calendar = Redmine::Helpers::Calendar.new(Date.civil(@year, @month, 1), current_language, :month)
365 @calendar = Redmine::Helpers::Calendar.new(Date.civil(@year, @month, 1), current_language, :month)
366 retrieve_query
366 retrieve_query
367 if @query.valid?
367 if @query.valid?
368 events = []
368 events = []
369 events += Issue.find(:all,
369 events += Issue.find(:all,
370 :include => [:tracker, :status, :assigned_to, :priority, :project],
370 :include => [:tracker, :status, :assigned_to, :priority, :project],
371 :conditions => ["(#{@query.statement}) AND ((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?))", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt]
371 :conditions => ["(#{@query.statement}) AND ((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?))", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt]
372 )
372 )
373 events += Version.find(:all, :include => :project,
373 events += Version.find(:all, :include => :project,
374 :conditions => ["(#{@query.project_statement}) AND effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt])
374 :conditions => ["(#{@query.project_statement}) AND effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt])
375
375
376 @calendar.events = events
376 @calendar.events = events
377 end
377 end
378
378
379 render :layout => false if request.xhr?
379 render :layout => false if request.xhr?
380 end
380 end
381
381
382 def context_menu
382 def context_menu
383 @issues = Issue.find_all_by_id(params[:ids], :include => :project)
383 @issues = Issue.find_all_by_id(params[:ids], :include => :project)
384 if (@issues.size == 1)
384 if (@issues.size == 1)
385 @issue = @issues.first
385 @issue = @issues.first
386 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
386 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
387 end
387 end
388 projects = @issues.collect(&:project).compact.uniq
388 projects = @issues.collect(&:project).compact.uniq
389 @project = projects.first if projects.size == 1
389 @project = projects.first if projects.size == 1
390
390
391 @can = {:edit => (@project && User.current.allowed_to?(:edit_issues, @project)),
391 @can = {:edit => (@project && User.current.allowed_to?(:edit_issues, @project)),
392 :log_time => (@project && User.current.allowed_to?(:log_time, @project)),
392 :log_time => (@project && User.current.allowed_to?(:log_time, @project)),
393 :update => (@project && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && @allowed_statuses && !@allowed_statuses.empty?))),
393 :update => (@project && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && @allowed_statuses && !@allowed_statuses.empty?))),
394 :move => (@project && User.current.allowed_to?(:move_issues, @project)),
394 :move => (@project && User.current.allowed_to?(:move_issues, @project)),
395 :copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
395 :copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
396 :delete => (@project && User.current.allowed_to?(:delete_issues, @project))
396 :delete => (@project && User.current.allowed_to?(:delete_issues, @project))
397 }
397 }
398 if @project
398 if @project
399 @assignables = @project.assignable_users
399 @assignables = @project.assignable_users
400 @assignables << @issue.assigned_to if @issue && @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
400 @assignables << @issue.assigned_to if @issue && @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
401 end
401 end
402
402
403 @priorities = Enumeration.get_values('IPRI').reverse
403 @priorities = Enumeration.get_values('IPRI').reverse
404 @statuses = IssueStatus.find(:all, :order => 'position')
404 @statuses = IssueStatus.find(:all, :order => 'position')
405 @back = request.env['HTTP_REFERER']
405 @back = request.env['HTTP_REFERER']
406
406
407 render :layout => false
407 render :layout => false
408 end
408 end
409
409
410 def update_form
410 def update_form
411 @issue = Issue.new(params[:issue])
411 @issue = Issue.new(params[:issue])
412 render :action => :new, :layout => false
412 render :action => :new, :layout => false
413 end
413 end
414
414
415 def preview
415 def preview
416 @issue = @project.issues.find_by_id(params[:id]) unless params[:id].blank?
416 @issue = @project.issues.find_by_id(params[:id]) unless params[:id].blank?
417 @attachements = @issue.attachments if @issue
417 @attachements = @issue.attachments if @issue
418 @text = params[:notes] || (params[:issue] ? params[:issue][:description] : nil)
418 @text = params[:notes] || (params[:issue] ? params[:issue][:description] : nil)
419 render :partial => 'common/preview'
419 render :partial => 'common/preview'
420 end
420 end
421
421
422 private
422 private
423 def find_issue
423 def find_issue
424 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
424 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
425 @project = @issue.project
425 @project = @issue.project
426 rescue ActiveRecord::RecordNotFound
426 rescue ActiveRecord::RecordNotFound
427 render_404
427 render_404
428 end
428 end
429
429
430 # Filter for bulk operations
430 # Filter for bulk operations
431 def find_issues
431 def find_issues
432 @issues = Issue.find_all_by_id(params[:id] || params[:ids])
432 @issues = Issue.find_all_by_id(params[:id] || params[:ids])
433 raise ActiveRecord::RecordNotFound if @issues.empty?
433 raise ActiveRecord::RecordNotFound if @issues.empty?
434 projects = @issues.collect(&:project).compact.uniq
434 projects = @issues.collect(&:project).compact.uniq
435 if projects.size == 1
435 if projects.size == 1
436 @project = projects.first
436 @project = projects.first
437 else
437 else
438 # TODO: let users bulk edit/move/destroy issues from different projects
438 # TODO: let users bulk edit/move/destroy issues from different projects
439 render_error 'Can not bulk edit/move/destroy issues from different projects' and return false
439 render_error 'Can not bulk edit/move/destroy issues from different projects' and return false
440 end
440 end
441 rescue ActiveRecord::RecordNotFound
441 rescue ActiveRecord::RecordNotFound
442 render_404
442 render_404
443 end
443 end
444
444
445 def find_project
445 def find_project
446 @project = Project.find(params[:project_id])
446 @project = Project.find(params[:project_id])
447 rescue ActiveRecord::RecordNotFound
447 rescue ActiveRecord::RecordNotFound
448 render_404
448 render_404
449 end
449 end
450
450
451 def find_optional_project
451 def find_optional_project
452 @project = Project.find(params[:project_id]) unless params[:project_id].blank?
452 @project = Project.find(params[:project_id]) unless params[:project_id].blank?
453 allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
453 allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
454 allowed ? true : deny_access
454 allowed ? true : deny_access
455 rescue ActiveRecord::RecordNotFound
455 rescue ActiveRecord::RecordNotFound
456 render_404
456 render_404
457 end
457 end
458
458
459 # Retrieve query from session or build a new query
459 # Retrieve query from session or build a new query
460 def retrieve_query
460 def retrieve_query
461 if !params[:query_id].blank?
461 if !params[:query_id].blank?
462 cond = "project_id IS NULL"
462 cond = "project_id IS NULL"
463 cond << " OR project_id = #{@project.id}" if @project
463 cond << " OR project_id = #{@project.id}" if @project
464 @query = Query.find(params[:query_id], :conditions => cond)
464 @query = Query.find(params[:query_id], :conditions => cond)
465 @query.project = @project
465 @query.project = @project
466 session[:query] = {:id => @query.id, :project_id => @query.project_id}
466 session[:query] = {:id => @query.id, :project_id => @query.project_id}
467 else
467 else
468 if params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
468 if params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
469 # Give it a name, required to be valid
469 # Give it a name, required to be valid
470 @query = Query.new(:name => "_")
470 @query = Query.new(:name => "_")
471 @query.project = @project
471 @query.project = @project
472 if params[:fields] and params[:fields].is_a? Array
472 if params[:fields] and params[:fields].is_a? Array
473 params[:fields].each do |field|
473 params[:fields].each do |field|
474 @query.add_filter(field, params[:operators][field], params[:values][field])
474 @query.add_filter(field, params[:operators][field], params[:values][field])
475 end
475 end
476 else
476 else
477 @query.available_filters.keys.each do |field|
477 @query.available_filters.keys.each do |field|
478 @query.add_short_filter(field, params[field]) if params[field]
478 @query.add_short_filter(field, params[field]) if params[field]
479 end
479 end
480 end
480 end
481 session[:query] = {:project_id => @query.project_id, :filters => @query.filters}
481 session[:query] = {:project_id => @query.project_id, :filters => @query.filters}
482 else
482 else
483 @query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
483 @query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
484 @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters])
484 @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters])
485 @query.project = @project
485 @query.project = @project
486 end
486 end
487 end
487 end
488 end
488 end
489 end
489 end
@@ -1,281 +1,290
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 => 'Enumeration', :foreign_key => 'priority_id'
25 belongs_to :priority, :class_name => 'Enumeration', :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.subject}"},
42 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id}: #{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 def after_initialize
54 def after_initialize
55 if new_record?
55 if new_record?
56 # set default values for new records only
56 # set default values for new records only
57 self.status ||= IssueStatus.default
57 self.status ||= IssueStatus.default
58 self.priority ||= Enumeration.default('IPRI')
58 self.priority ||= Enumeration.default('IPRI')
59 end
59 end
60 end
60 end
61
61
62 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
62 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
63 def available_custom_fields
63 def available_custom_fields
64 (project && tracker) ? project.all_issue_custom_fields.select {|c| tracker.custom_fields.include? c } : []
64 (project && tracker) ? project.all_issue_custom_fields.select {|c| tracker.custom_fields.include? c } : []
65 end
65 end
66
66
67 def copy_from(arg)
67 def copy_from(arg)
68 issue = arg.is_a?(Issue) ? arg : Issue.find(arg)
68 issue = arg.is_a?(Issue) ? arg : Issue.find(arg)
69 self.attributes = issue.attributes.dup
69 self.attributes = issue.attributes.dup
70 self.custom_values = issue.custom_values.collect {|v| v.clone}
70 self.custom_values = issue.custom_values.collect {|v| v.clone}
71 self
71 self
72 end
72 end
73
73
74 # Move an issue to a new project and tracker
74 # Moves/copies an issue to a new project and tracker
75 def move_to(new_project, new_tracker = nil)
75 # Returns the moved/copied issue on success, false on failure
76 def move_to(new_project, new_tracker = nil, options = {})
77 options ||= {}
78 issue = options[:copy] ? self.clone : self
76 transaction do
79 transaction do
77 if new_project && project_id != new_project.id
80 if new_project && issue.project_id != new_project.id
78 # delete issue relations
81 # delete issue relations
79 unless Setting.cross_project_issue_relations?
82 unless Setting.cross_project_issue_relations?
80 self.relations_from.clear
83 issue.relations_from.clear
81 self.relations_to.clear
84 issue.relations_to.clear
82 end
85 end
83 # issue is moved to another project
86 # issue is moved to another project
84 # reassign to the category with same name if any
87 # reassign to the category with same name if any
85 new_category = category.nil? ? nil : new_project.issue_categories.find_by_name(category.name)
88 new_category = issue.category.nil? ? nil : new_project.issue_categories.find_by_name(issue.category.name)
86 self.category = new_category
89 issue.category = new_category
87 self.fixed_version = nil
90 issue.fixed_version = nil
88 self.project = new_project
91 issue.project = new_project
89 end
92 end
90 if new_tracker
93 if new_tracker
91 self.tracker = new_tracker
94 issue.tracker = new_tracker
92 end
95 end
93 if save
96 if options[:copy]
94 # Manually update project_id on related time entries
97 issue.custom_field_values = self.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
95 TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
98 issue.status = self.status
99 end
100 if issue.save
101 unless options[:copy]
102 # Manually update project_id on related time entries
103 TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
104 end
96 else
105 else
97 rollback_db_transaction
106 Issue.connection.rollback_db_transaction
98 return false
107 return false
99 end
108 end
100 end
109 end
101 return true
110 return issue
102 end
111 end
103
112
104 def priority_id=(pid)
113 def priority_id=(pid)
105 self.priority = nil
114 self.priority = nil
106 write_attribute(:priority_id, pid)
115 write_attribute(:priority_id, pid)
107 end
116 end
108
117
109 def estimated_hours=(h)
118 def estimated_hours=(h)
110 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
119 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
111 end
120 end
112
121
113 def validate
122 def validate
114 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
123 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
115 errors.add :due_date, :activerecord_error_not_a_date
124 errors.add :due_date, :activerecord_error_not_a_date
116 end
125 end
117
126
118 if self.due_date and self.start_date and self.due_date < self.start_date
127 if self.due_date and self.start_date and self.due_date < self.start_date
119 errors.add :due_date, :activerecord_error_greater_than_start_date
128 errors.add :due_date, :activerecord_error_greater_than_start_date
120 end
129 end
121
130
122 if start_date && soonest_start && start_date < soonest_start
131 if start_date && soonest_start && start_date < soonest_start
123 errors.add :start_date, :activerecord_error_invalid
132 errors.add :start_date, :activerecord_error_invalid
124 end
133 end
125 end
134 end
126
135
127 def validate_on_create
136 def validate_on_create
128 errors.add :tracker_id, :activerecord_error_invalid unless project.trackers.include?(tracker)
137 errors.add :tracker_id, :activerecord_error_invalid unless project.trackers.include?(tracker)
129 end
138 end
130
139
131 def before_create
140 def before_create
132 # default assignment based on category
141 # default assignment based on category
133 if assigned_to.nil? && category && category.assigned_to
142 if assigned_to.nil? && category && category.assigned_to
134 self.assigned_to = category.assigned_to
143 self.assigned_to = category.assigned_to
135 end
144 end
136 end
145 end
137
146
138 def before_save
147 def before_save
139 if @current_journal
148 if @current_journal
140 # attributes changes
149 # attributes changes
141 (Issue.column_names - %w(id description)).each {|c|
150 (Issue.column_names - %w(id description)).each {|c|
142 @current_journal.details << JournalDetail.new(:property => 'attr',
151 @current_journal.details << JournalDetail.new(:property => 'attr',
143 :prop_key => c,
152 :prop_key => c,
144 :old_value => @issue_before_change.send(c),
153 :old_value => @issue_before_change.send(c),
145 :value => send(c)) unless send(c)==@issue_before_change.send(c)
154 :value => send(c)) unless send(c)==@issue_before_change.send(c)
146 }
155 }
147 # custom fields changes
156 # custom fields changes
148 custom_values.each {|c|
157 custom_values.each {|c|
149 next if (@custom_values_before_change[c.custom_field_id]==c.value ||
158 next if (@custom_values_before_change[c.custom_field_id]==c.value ||
150 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
159 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
151 @current_journal.details << JournalDetail.new(:property => 'cf',
160 @current_journal.details << JournalDetail.new(:property => 'cf',
152 :prop_key => c.custom_field_id,
161 :prop_key => c.custom_field_id,
153 :old_value => @custom_values_before_change[c.custom_field_id],
162 :old_value => @custom_values_before_change[c.custom_field_id],
154 :value => c.value)
163 :value => c.value)
155 }
164 }
156 @current_journal.save
165 @current_journal.save
157 end
166 end
158 # Save the issue even if the journal is not saved (because empty)
167 # Save the issue even if the journal is not saved (because empty)
159 true
168 true
160 end
169 end
161
170
162 def after_save
171 def after_save
163 # Reload is needed in order to get the right status
172 # Reload is needed in order to get the right status
164 reload
173 reload
165
174
166 # Update start/due dates of following issues
175 # Update start/due dates of following issues
167 relations_from.each(&:set_issue_to_dates)
176 relations_from.each(&:set_issue_to_dates)
168
177
169 # Close duplicates if the issue was closed
178 # Close duplicates if the issue was closed
170 if @issue_before_change && !@issue_before_change.closed? && self.closed?
179 if @issue_before_change && !@issue_before_change.closed? && self.closed?
171 duplicates.each do |duplicate|
180 duplicates.each do |duplicate|
172 # Reload is need in case the duplicate was updated by a previous duplicate
181 # Reload is need in case the duplicate was updated by a previous duplicate
173 duplicate.reload
182 duplicate.reload
174 # Don't re-close it if it's already closed
183 # Don't re-close it if it's already closed
175 next if duplicate.closed?
184 next if duplicate.closed?
176 # Same user and notes
185 # Same user and notes
177 duplicate.init_journal(@current_journal.user, @current_journal.notes)
186 duplicate.init_journal(@current_journal.user, @current_journal.notes)
178 duplicate.update_attribute :status, self.status
187 duplicate.update_attribute :status, self.status
179 end
188 end
180 end
189 end
181 end
190 end
182
191
183 def init_journal(user, notes = "")
192 def init_journal(user, notes = "")
184 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
193 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
185 @issue_before_change = self.clone
194 @issue_before_change = self.clone
186 @issue_before_change.status = self.status
195 @issue_before_change.status = self.status
187 @custom_values_before_change = {}
196 @custom_values_before_change = {}
188 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
197 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
189 # Make sure updated_on is updated when adding a note.
198 # Make sure updated_on is updated when adding a note.
190 updated_on_will_change!
199 updated_on_will_change!
191 @current_journal
200 @current_journal
192 end
201 end
193
202
194 # Return true if the issue is closed, otherwise false
203 # Return true if the issue is closed, otherwise false
195 def closed?
204 def closed?
196 self.status.is_closed?
205 self.status.is_closed?
197 end
206 end
198
207
199 # Returns true if the issue is overdue
208 # Returns true if the issue is overdue
200 def overdue?
209 def overdue?
201 !due_date.nil? && (due_date < Date.today)
210 !due_date.nil? && (due_date < Date.today)
202 end
211 end
203
212
204 # Users the issue can be assigned to
213 # Users the issue can be assigned to
205 def assignable_users
214 def assignable_users
206 project.assignable_users
215 project.assignable_users
207 end
216 end
208
217
209 # Returns an array of status that user is able to apply
218 # Returns an array of status that user is able to apply
210 def new_statuses_allowed_to(user)
219 def new_statuses_allowed_to(user)
211 statuses = status.find_new_statuses_allowed_to(user.role_for_project(project), tracker)
220 statuses = status.find_new_statuses_allowed_to(user.role_for_project(project), tracker)
212 statuses << status unless statuses.empty?
221 statuses << status unless statuses.empty?
213 statuses.uniq.sort
222 statuses.uniq.sort
214 end
223 end
215
224
216 # Returns the mail adresses of users that should be notified for the issue
225 # Returns the mail adresses of users that should be notified for the issue
217 def recipients
226 def recipients
218 recipients = project.recipients
227 recipients = project.recipients
219 # Author and assignee are always notified unless they have been locked
228 # Author and assignee are always notified unless they have been locked
220 recipients << author.mail if author && author.active?
229 recipients << author.mail if author && author.active?
221 recipients << assigned_to.mail if assigned_to && assigned_to.active?
230 recipients << assigned_to.mail if assigned_to && assigned_to.active?
222 recipients.compact.uniq
231 recipients.compact.uniq
223 end
232 end
224
233
225 def spent_hours
234 def spent_hours
226 @spent_hours ||= time_entries.sum(:hours) || 0
235 @spent_hours ||= time_entries.sum(:hours) || 0
227 end
236 end
228
237
229 def relations
238 def relations
230 (relations_from + relations_to).sort
239 (relations_from + relations_to).sort
231 end
240 end
232
241
233 def all_dependent_issues
242 def all_dependent_issues
234 dependencies = []
243 dependencies = []
235 relations_from.each do |relation|
244 relations_from.each do |relation|
236 dependencies << relation.issue_to
245 dependencies << relation.issue_to
237 dependencies += relation.issue_to.all_dependent_issues
246 dependencies += relation.issue_to.all_dependent_issues
238 end
247 end
239 dependencies
248 dependencies
240 end
249 end
241
250
242 # Returns an array of issues that duplicate this one
251 # Returns an array of issues that duplicate this one
243 def duplicates
252 def duplicates
244 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
253 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
245 end
254 end
246
255
247 # Returns the due date or the target due date if any
256 # Returns the due date or the target due date if any
248 # Used on gantt chart
257 # Used on gantt chart
249 def due_before
258 def due_before
250 due_date || (fixed_version ? fixed_version.effective_date : nil)
259 due_date || (fixed_version ? fixed_version.effective_date : nil)
251 end
260 end
252
261
253 def duration
262 def duration
254 (start_date && due_date) ? due_date - start_date : 0
263 (start_date && due_date) ? due_date - start_date : 0
255 end
264 end
256
265
257 def soonest_start
266 def soonest_start
258 @soonest_start ||= relations_to.collect{|relation| relation.successor_soonest_start}.compact.min
267 @soonest_start ||= relations_to.collect{|relation| relation.successor_soonest_start}.compact.min
259 end
268 end
260
269
261 def self.visible_by(usr)
270 def self.visible_by(usr)
262 with_scope(:find => { :conditions => Project.visible_by(usr) }) do
271 with_scope(:find => { :conditions => Project.visible_by(usr) }) do
263 yield
272 yield
264 end
273 end
265 end
274 end
266
275
267 def to_s
276 def to_s
268 "#{tracker} ##{id}: #{subject}"
277 "#{tracker} ##{id}: #{subject}"
269 end
278 end
270
279
271 private
280 private
272
281
273 # Callback on attachment deletion
282 # Callback on attachment deletion
274 def attachment_removed(obj)
283 def attachment_removed(obj)
275 journal = init_journal(User.current)
284 journal = init_journal(User.current)
276 journal.details << JournalDetail.new(:property => 'attachment',
285 journal.details << JournalDetail.new(:property => 'attachment',
277 :prop_key => obj.id,
286 :prop_key => obj.id,
278 :old_value => obj.filename)
287 :old_value => obj.filename)
279 journal.save
288 journal.save
280 end
289 end
281 end
290 end
@@ -1,22 +1,25
1 <h2><%= l(:button_move) %></h2>
1 <h2><%= l(:button_move) %></h2>
2
2
3 <ul><%= @issues.collect {|i| content_tag('li', link_to(h("#{i.tracker} ##{i.id}"), { :action => 'show', :id => i }) + h(": #{i.subject}")) }.join("\n") %></ul>
3 <ul><%= @issues.collect {|i| content_tag('li', link_to(h("#{i.tracker} ##{i.id}"), { :action => 'show', :id => i }) + h(": #{i.subject}")) }.join("\n") %></ul>
4
4
5 <% form_tag({}, :id => 'move_form') do %>
5 <% form_tag({}, :id => 'move_form') do %>
6 <%= @issues.collect {|i| hidden_field_tag('ids[]', i.id)}.join %>
6 <%= @issues.collect {|i| hidden_field_tag('ids[]', i.id)}.join %>
7
7
8 <div class="box tabular">
8 <div class="box tabular">
9 <p><label for="new_project_id"><%=l(:field_project)%> :</label>
9 <p><label for="new_project_id"><%=l(:field_project)%> :</label>
10 <%= select_tag "new_project_id",
10 <%= select_tag "new_project_id",
11 options_from_collection_for_select(@allowed_projects, 'id', 'name', @target_project.id),
11 options_from_collection_for_select(@allowed_projects, 'id', 'name', @target_project.id),
12 :onchange => remote_function(:url => { :action => 'move' },
12 :onchange => remote_function(:url => { :action => 'move' },
13 :method => :get,
13 :method => :get,
14 :update => 'content',
14 :update => 'content',
15 :with => "Form.serialize('move_form')") %></p>
15 :with => "Form.serialize('move_form')") %></p>
16
16
17 <p><label for="new_tracker_id"><%=l(:field_tracker)%> :</label>
17 <p><label for="new_tracker_id"><%=l(:field_tracker)%> :</label>
18 <%= select_tag "new_tracker_id", "<option value=\"\">#{l(:label_no_change_option)}</option>" + options_from_collection_for_select(@trackers, "id", "name") %></p>
18 <%= select_tag "new_tracker_id", "<option value=\"\">#{l(:label_no_change_option)}</option>" + options_from_collection_for_select(@trackers, "id", "name") %></p>
19
20 <p><label for="copy_options_copy"><%= l(:button_copy)%></label>
21 <%= check_box_tag "copy_options[copy]", "1" %></p>
19 </div>
22 </div>
20
23
21 <%= submit_tag l(:button_move) %>
24 <%= submit_tag l(:button_move) %>
22 <% end %>
25 <% end %>
@@ -1,787 +1,797
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 < Test::Unit::TestCase
24 class IssuesControllerTest < Test::Unit::TestCase
25 fixtures :projects,
25 fixtures :projects,
26 :users,
26 :users,
27 :roles,
27 :roles,
28 :members,
28 :members,
29 :issues,
29 :issues,
30 :issue_statuses,
30 :issue_statuses,
31 :versions,
31 :versions,
32 :trackers,
32 :trackers,
33 :projects_trackers,
33 :projects_trackers,
34 :issue_categories,
34 :issue_categories,
35 :enabled_modules,
35 :enabled_modules,
36 :enumerations,
36 :enumerations,
37 :attachments,
37 :attachments,
38 :workflows,
38 :workflows,
39 :custom_fields,
39 :custom_fields,
40 :custom_values,
40 :custom_values,
41 :custom_fields_trackers,
41 :custom_fields_trackers,
42 :time_entries,
42 :time_entries,
43 :journals,
43 :journals,
44 :journal_details
44 :journal_details
45
45
46 def setup
46 def setup
47 @controller = IssuesController.new
47 @controller = IssuesController.new
48 @request = ActionController::TestRequest.new
48 @request = ActionController::TestRequest.new
49 @response = ActionController::TestResponse.new
49 @response = ActionController::TestResponse.new
50 User.current = nil
50 User.current = nil
51 end
51 end
52
52
53 def test_index
53 def test_index
54 get :index
54 get :index
55 assert_response :success
55 assert_response :success
56 assert_template 'index.rhtml'
56 assert_template 'index.rhtml'
57 assert_not_nil assigns(:issues)
57 assert_not_nil assigns(:issues)
58 assert_nil assigns(:project)
58 assert_nil assigns(:project)
59 assert_tag :tag => 'a', :content => /Can't print recipes/
59 assert_tag :tag => 'a', :content => /Can't print recipes/
60 assert_tag :tag => 'a', :content => /Subproject issue/
60 assert_tag :tag => 'a', :content => /Subproject issue/
61 # private projects hidden
61 # private projects hidden
62 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
62 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
63 assert_no_tag :tag => 'a', :content => /Issue on project 2/
63 assert_no_tag :tag => 'a', :content => /Issue on project 2/
64 end
64 end
65
65
66 def test_index_should_not_list_issues_when_module_disabled
66 def test_index_should_not_list_issues_when_module_disabled
67 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
67 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
68 get :index
68 get :index
69 assert_response :success
69 assert_response :success
70 assert_template 'index.rhtml'
70 assert_template 'index.rhtml'
71 assert_not_nil assigns(:issues)
71 assert_not_nil assigns(:issues)
72 assert_nil assigns(:project)
72 assert_nil assigns(:project)
73 assert_no_tag :tag => 'a', :content => /Can't print recipes/
73 assert_no_tag :tag => 'a', :content => /Can't print recipes/
74 assert_tag :tag => 'a', :content => /Subproject issue/
74 assert_tag :tag => 'a', :content => /Subproject issue/
75 end
75 end
76
76
77 def test_index_with_project
77 def test_index_with_project
78 Setting.display_subprojects_issues = 0
78 Setting.display_subprojects_issues = 0
79 get :index, :project_id => 1
79 get :index, :project_id => 1
80 assert_response :success
80 assert_response :success
81 assert_template 'index.rhtml'
81 assert_template 'index.rhtml'
82 assert_not_nil assigns(:issues)
82 assert_not_nil assigns(:issues)
83 assert_tag :tag => 'a', :content => /Can't print recipes/
83 assert_tag :tag => 'a', :content => /Can't print recipes/
84 assert_no_tag :tag => 'a', :content => /Subproject issue/
84 assert_no_tag :tag => 'a', :content => /Subproject issue/
85 end
85 end
86
86
87 def test_index_with_project_and_subprojects
87 def test_index_with_project_and_subprojects
88 Setting.display_subprojects_issues = 1
88 Setting.display_subprojects_issues = 1
89 get :index, :project_id => 1
89 get :index, :project_id => 1
90 assert_response :success
90 assert_response :success
91 assert_template 'index.rhtml'
91 assert_template 'index.rhtml'
92 assert_not_nil assigns(:issues)
92 assert_not_nil assigns(:issues)
93 assert_tag :tag => 'a', :content => /Can't print recipes/
93 assert_tag :tag => 'a', :content => /Can't print recipes/
94 assert_tag :tag => 'a', :content => /Subproject issue/
94 assert_tag :tag => 'a', :content => /Subproject issue/
95 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
95 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
96 end
96 end
97
97
98 def test_index_with_project_and_subprojects_should_show_private_subprojects
98 def test_index_with_project_and_subprojects_should_show_private_subprojects
99 @request.session[:user_id] = 2
99 @request.session[:user_id] = 2
100 Setting.display_subprojects_issues = 1
100 Setting.display_subprojects_issues = 1
101 get :index, :project_id => 1
101 get :index, :project_id => 1
102 assert_response :success
102 assert_response :success
103 assert_template 'index.rhtml'
103 assert_template 'index.rhtml'
104 assert_not_nil assigns(:issues)
104 assert_not_nil assigns(:issues)
105 assert_tag :tag => 'a', :content => /Can't print recipes/
105 assert_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 assert_tag :tag => 'a', :content => /Issue of a private subproject/
107 assert_tag :tag => 'a', :content => /Issue of a private subproject/
108 end
108 end
109
109
110 def test_index_with_project_and_filter
110 def test_index_with_project_and_filter
111 get :index, :project_id => 1, :set_filter => 1
111 get :index, :project_id => 1, :set_filter => 1
112 assert_response :success
112 assert_response :success
113 assert_template 'index.rhtml'
113 assert_template 'index.rhtml'
114 assert_not_nil assigns(:issues)
114 assert_not_nil assigns(:issues)
115 end
115 end
116
116
117 def test_index_csv_with_project
117 def test_index_csv_with_project
118 get :index, :format => 'csv'
118 get :index, :format => 'csv'
119 assert_response :success
119 assert_response :success
120 assert_not_nil assigns(:issues)
120 assert_not_nil assigns(:issues)
121 assert_equal 'text/csv', @response.content_type
121 assert_equal 'text/csv', @response.content_type
122
122
123 get :index, :project_id => 1, :format => 'csv'
123 get :index, :project_id => 1, :format => 'csv'
124 assert_response :success
124 assert_response :success
125 assert_not_nil assigns(:issues)
125 assert_not_nil assigns(:issues)
126 assert_equal 'text/csv', @response.content_type
126 assert_equal 'text/csv', @response.content_type
127 end
127 end
128
128
129 def test_index_pdf
129 def test_index_pdf
130 get :index, :format => 'pdf'
130 get :index, :format => 'pdf'
131 assert_response :success
131 assert_response :success
132 assert_not_nil assigns(:issues)
132 assert_not_nil assigns(:issues)
133 assert_equal 'application/pdf', @response.content_type
133 assert_equal 'application/pdf', @response.content_type
134
134
135 get :index, :project_id => 1, :format => 'pdf'
135 get :index, :project_id => 1, :format => 'pdf'
136 assert_response :success
136 assert_response :success
137 assert_not_nil assigns(:issues)
137 assert_not_nil assigns(:issues)
138 assert_equal 'application/pdf', @response.content_type
138 assert_equal 'application/pdf', @response.content_type
139 end
139 end
140
140
141 def test_index_sort
141 def test_index_sort
142 get :index, :sort_key => 'tracker'
142 get :index, :sort_key => 'tracker'
143 assert_response :success
143 assert_response :success
144
144
145 sort_params = @request.session['issuesindex_sort']
145 sort_params = @request.session['issuesindex_sort']
146 assert sort_params.is_a?(Hash)
146 assert sort_params.is_a?(Hash)
147 assert_equal 'tracker', sort_params[:key]
147 assert_equal 'tracker', sort_params[:key]
148 assert_equal 'ASC', sort_params[:order]
148 assert_equal 'ASC', sort_params[:order]
149 end
149 end
150
150
151 def test_gantt
151 def test_gantt
152 get :gantt, :project_id => 1
152 get :gantt, :project_id => 1
153 assert_response :success
153 assert_response :success
154 assert_template 'gantt.rhtml'
154 assert_template 'gantt.rhtml'
155 assert_not_nil assigns(:gantt)
155 assert_not_nil assigns(:gantt)
156 events = assigns(:gantt).events
156 events = assigns(:gantt).events
157 assert_not_nil events
157 assert_not_nil events
158 # Issue with start and due dates
158 # Issue with start and due dates
159 i = Issue.find(1)
159 i = Issue.find(1)
160 assert_not_nil i.due_date
160 assert_not_nil i.due_date
161 assert events.include?(Issue.find(1))
161 assert events.include?(Issue.find(1))
162 # Issue with without due date but targeted to a version with date
162 # Issue with without due date but targeted to a version with date
163 i = Issue.find(2)
163 i = Issue.find(2)
164 assert_nil i.due_date
164 assert_nil i.due_date
165 assert events.include?(i)
165 assert events.include?(i)
166 end
166 end
167
167
168 def test_cross_project_gantt
168 def test_cross_project_gantt
169 get :gantt
169 get :gantt
170 assert_response :success
170 assert_response :success
171 assert_template 'gantt.rhtml'
171 assert_template 'gantt.rhtml'
172 assert_not_nil assigns(:gantt)
172 assert_not_nil assigns(:gantt)
173 events = assigns(:gantt).events
173 events = assigns(:gantt).events
174 assert_not_nil events
174 assert_not_nil events
175 end
175 end
176
176
177 def test_gantt_export_to_pdf
177 def test_gantt_export_to_pdf
178 get :gantt, :project_id => 1, :format => 'pdf'
178 get :gantt, :project_id => 1, :format => 'pdf'
179 assert_response :success
179 assert_response :success
180 assert_equal 'application/pdf', @response.content_type
180 assert_equal 'application/pdf', @response.content_type
181 assert @response.body.starts_with?('%PDF')
181 assert @response.body.starts_with?('%PDF')
182 assert_not_nil assigns(:gantt)
182 assert_not_nil assigns(:gantt)
183 end
183 end
184
184
185 def test_cross_project_gantt_export_to_pdf
185 def test_cross_project_gantt_export_to_pdf
186 get :gantt, :format => 'pdf'
186 get :gantt, :format => 'pdf'
187 assert_response :success
187 assert_response :success
188 assert_equal 'application/pdf', @response.content_type
188 assert_equal 'application/pdf', @response.content_type
189 assert @response.body.starts_with?('%PDF')
189 assert @response.body.starts_with?('%PDF')
190 assert_not_nil assigns(:gantt)
190 assert_not_nil assigns(:gantt)
191 end
191 end
192
192
193 if Object.const_defined?(:Magick)
193 if Object.const_defined?(:Magick)
194 def test_gantt_image
194 def test_gantt_image
195 get :gantt, :project_id => 1, :format => 'png'
195 get :gantt, :project_id => 1, :format => 'png'
196 assert_response :success
196 assert_response :success
197 assert_equal 'image/png', @response.content_type
197 assert_equal 'image/png', @response.content_type
198 end
198 end
199 else
199 else
200 puts "RMagick not installed. Skipping tests !!!"
200 puts "RMagick not installed. Skipping tests !!!"
201 end
201 end
202
202
203 def test_calendar
203 def test_calendar
204 get :calendar, :project_id => 1
204 get :calendar, :project_id => 1
205 assert_response :success
205 assert_response :success
206 assert_template 'calendar'
206 assert_template 'calendar'
207 assert_not_nil assigns(:calendar)
207 assert_not_nil assigns(:calendar)
208 end
208 end
209
209
210 def test_cross_project_calendar
210 def test_cross_project_calendar
211 get :calendar
211 get :calendar
212 assert_response :success
212 assert_response :success
213 assert_template 'calendar'
213 assert_template 'calendar'
214 assert_not_nil assigns(:calendar)
214 assert_not_nil assigns(:calendar)
215 end
215 end
216
216
217 def test_changes
217 def test_changes
218 get :changes, :project_id => 1
218 get :changes, :project_id => 1
219 assert_response :success
219 assert_response :success
220 assert_not_nil assigns(:journals)
220 assert_not_nil assigns(:journals)
221 assert_equal 'application/atom+xml', @response.content_type
221 assert_equal 'application/atom+xml', @response.content_type
222 end
222 end
223
223
224 def test_show_by_anonymous
224 def test_show_by_anonymous
225 get :show, :id => 1
225 get :show, :id => 1
226 assert_response :success
226 assert_response :success
227 assert_template 'show.rhtml'
227 assert_template 'show.rhtml'
228 assert_not_nil assigns(:issue)
228 assert_not_nil assigns(:issue)
229 assert_equal Issue.find(1), assigns(:issue)
229 assert_equal Issue.find(1), assigns(:issue)
230
230
231 # anonymous role is allowed to add a note
231 # anonymous role is allowed to add a note
232 assert_tag :tag => 'form',
232 assert_tag :tag => 'form',
233 :descendant => { :tag => 'fieldset',
233 :descendant => { :tag => 'fieldset',
234 :child => { :tag => 'legend',
234 :child => { :tag => 'legend',
235 :content => /Notes/ } }
235 :content => /Notes/ } }
236 end
236 end
237
237
238 def test_show_by_manager
238 def test_show_by_manager
239 @request.session[:user_id] = 2
239 @request.session[:user_id] = 2
240 get :show, :id => 1
240 get :show, :id => 1
241 assert_response :success
241 assert_response :success
242
242
243 assert_tag :tag => 'form',
243 assert_tag :tag => 'form',
244 :descendant => { :tag => 'fieldset',
244 :descendant => { :tag => 'fieldset',
245 :child => { :tag => 'legend',
245 :child => { :tag => 'legend',
246 :content => /Change properties/ } },
246 :content => /Change properties/ } },
247 :descendant => { :tag => 'fieldset',
247 :descendant => { :tag => 'fieldset',
248 :child => { :tag => 'legend',
248 :child => { :tag => 'legend',
249 :content => /Log time/ } },
249 :content => /Log time/ } },
250 :descendant => { :tag => 'fieldset',
250 :descendant => { :tag => 'fieldset',
251 :child => { :tag => 'legend',
251 :child => { :tag => 'legend',
252 :content => /Notes/ } }
252 :content => /Notes/ } }
253 end
253 end
254
254
255 def test_show_export_to_pdf
255 def test_show_export_to_pdf
256 get :show, :id => 3, :format => 'pdf'
256 get :show, :id => 3, :format => 'pdf'
257 assert_response :success
257 assert_response :success
258 assert_equal 'application/pdf', @response.content_type
258 assert_equal 'application/pdf', @response.content_type
259 assert @response.body.starts_with?('%PDF')
259 assert @response.body.starts_with?('%PDF')
260 assert_not_nil assigns(:issue)
260 assert_not_nil assigns(:issue)
261 end
261 end
262
262
263 def test_get_new
263 def test_get_new
264 @request.session[:user_id] = 2
264 @request.session[:user_id] = 2
265 get :new, :project_id => 1, :tracker_id => 1
265 get :new, :project_id => 1, :tracker_id => 1
266 assert_response :success
266 assert_response :success
267 assert_template 'new'
267 assert_template 'new'
268
268
269 assert_tag :tag => 'input', :attributes => { :name => 'issue[custom_field_values][2]',
269 assert_tag :tag => 'input', :attributes => { :name => 'issue[custom_field_values][2]',
270 :value => 'Default string' }
270 :value => 'Default string' }
271 end
271 end
272
272
273 def test_get_new_without_tracker_id
273 def test_get_new_without_tracker_id
274 @request.session[:user_id] = 2
274 @request.session[:user_id] = 2
275 get :new, :project_id => 1
275 get :new, :project_id => 1
276 assert_response :success
276 assert_response :success
277 assert_template 'new'
277 assert_template 'new'
278
278
279 issue = assigns(:issue)
279 issue = assigns(:issue)
280 assert_not_nil issue
280 assert_not_nil issue
281 assert_equal Project.find(1).trackers.first, issue.tracker
281 assert_equal Project.find(1).trackers.first, issue.tracker
282 end
282 end
283
283
284 def test_update_new_form
284 def test_update_new_form
285 @request.session[:user_id] = 2
285 @request.session[:user_id] = 2
286 xhr :post, :new, :project_id => 1,
286 xhr :post, :new, :project_id => 1,
287 :issue => {:tracker_id => 2,
287 :issue => {:tracker_id => 2,
288 :subject => 'This is the test_new issue',
288 :subject => 'This is the test_new issue',
289 :description => 'This is the description',
289 :description => 'This is the description',
290 :priority_id => 5}
290 :priority_id => 5}
291 assert_response :success
291 assert_response :success
292 assert_template 'new'
292 assert_template 'new'
293 end
293 end
294
294
295 def test_post_new
295 def test_post_new
296 @request.session[:user_id] = 2
296 @request.session[:user_id] = 2
297 post :new, :project_id => 1,
297 post :new, :project_id => 1,
298 :issue => {:tracker_id => 3,
298 :issue => {:tracker_id => 3,
299 :subject => 'This is the test_new issue',
299 :subject => 'This is the test_new issue',
300 :description => 'This is the description',
300 :description => 'This is the description',
301 :priority_id => 5,
301 :priority_id => 5,
302 :estimated_hours => '',
302 :estimated_hours => '',
303 :custom_field_values => {'2' => 'Value for field 2'}}
303 :custom_field_values => {'2' => 'Value for field 2'}}
304 assert_redirected_to :controller => 'issues', :action => 'show'
304 assert_redirected_to :controller => 'issues', :action => 'show'
305
305
306 issue = Issue.find_by_subject('This is the test_new issue')
306 issue = Issue.find_by_subject('This is the test_new issue')
307 assert_not_nil issue
307 assert_not_nil issue
308 assert_equal 2, issue.author_id
308 assert_equal 2, issue.author_id
309 assert_equal 3, issue.tracker_id
309 assert_equal 3, issue.tracker_id
310 assert_nil issue.estimated_hours
310 assert_nil issue.estimated_hours
311 v = issue.custom_values.find(:first, :conditions => {:custom_field_id => 2})
311 v = issue.custom_values.find(:first, :conditions => {:custom_field_id => 2})
312 assert_not_nil v
312 assert_not_nil v
313 assert_equal 'Value for field 2', v.value
313 assert_equal 'Value for field 2', v.value
314 end
314 end
315
315
316 def test_post_new_and_continue
316 def test_post_new_and_continue
317 @request.session[:user_id] = 2
317 @request.session[:user_id] = 2
318 post :new, :project_id => 1,
318 post :new, :project_id => 1,
319 :issue => {:tracker_id => 3,
319 :issue => {:tracker_id => 3,
320 :subject => 'This is first issue',
320 :subject => 'This is first issue',
321 :priority_id => 5},
321 :priority_id => 5},
322 :continue => ''
322 :continue => ''
323 assert_redirected_to :controller => 'issues', :action => 'new', :tracker_id => 3
323 assert_redirected_to :controller => 'issues', :action => 'new', :tracker_id => 3
324 end
324 end
325
325
326 def test_post_new_without_custom_fields_param
326 def test_post_new_without_custom_fields_param
327 @request.session[:user_id] = 2
327 @request.session[:user_id] = 2
328 post :new, :project_id => 1,
328 post :new, :project_id => 1,
329 :issue => {:tracker_id => 1,
329 :issue => {:tracker_id => 1,
330 :subject => 'This is the test_new issue',
330 :subject => 'This is the test_new issue',
331 :description => 'This is the description',
331 :description => 'This is the description',
332 :priority_id => 5}
332 :priority_id => 5}
333 assert_redirected_to :controller => 'issues', :action => 'show'
333 assert_redirected_to :controller => 'issues', :action => 'show'
334 end
334 end
335
335
336 def test_post_new_with_required_custom_field_and_without_custom_fields_param
336 def test_post_new_with_required_custom_field_and_without_custom_fields_param
337 field = IssueCustomField.find_by_name('Database')
337 field = IssueCustomField.find_by_name('Database')
338 field.update_attribute(:is_required, true)
338 field.update_attribute(:is_required, true)
339
339
340 @request.session[:user_id] = 2
340 @request.session[:user_id] = 2
341 post :new, :project_id => 1,
341 post :new, :project_id => 1,
342 :issue => {:tracker_id => 1,
342 :issue => {:tracker_id => 1,
343 :subject => 'This is the test_new issue',
343 :subject => 'This is the test_new issue',
344 :description => 'This is the description',
344 :description => 'This is the description',
345 :priority_id => 5}
345 :priority_id => 5}
346 assert_response :success
346 assert_response :success
347 assert_template 'new'
347 assert_template 'new'
348 issue = assigns(:issue)
348 issue = assigns(:issue)
349 assert_not_nil issue
349 assert_not_nil issue
350 assert_equal 'activerecord_error_invalid', issue.errors.on(:custom_values)
350 assert_equal 'activerecord_error_invalid', issue.errors.on(:custom_values)
351 end
351 end
352
352
353 def test_post_new_with_watchers
353 def test_post_new_with_watchers
354 @request.session[:user_id] = 2
354 @request.session[:user_id] = 2
355 ActionMailer::Base.deliveries.clear
355 ActionMailer::Base.deliveries.clear
356
356
357 assert_difference 'Watcher.count', 2 do
357 assert_difference 'Watcher.count', 2 do
358 post :new, :project_id => 1,
358 post :new, :project_id => 1,
359 :issue => {:tracker_id => 1,
359 :issue => {:tracker_id => 1,
360 :subject => 'This is a new issue with watchers',
360 :subject => 'This is a new issue with watchers',
361 :description => 'This is the description',
361 :description => 'This is the description',
362 :priority_id => 5,
362 :priority_id => 5,
363 :watcher_user_ids => ['2', '3']}
363 :watcher_user_ids => ['2', '3']}
364 end
364 end
365 issue = Issue.find_by_subject('This is a new issue with watchers')
365 issue = Issue.find_by_subject('This is a new issue with watchers')
366 assert_not_nil issue
366 assert_not_nil issue
367 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
367 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
368
368
369 # Watchers added
369 # Watchers added
370 assert_equal [2, 3], issue.watcher_user_ids.sort
370 assert_equal [2, 3], issue.watcher_user_ids.sort
371 assert issue.watched_by?(User.find(3))
371 assert issue.watched_by?(User.find(3))
372 # Watchers notified
372 # Watchers notified
373 mail = ActionMailer::Base.deliveries.last
373 mail = ActionMailer::Base.deliveries.last
374 assert_kind_of TMail::Mail, mail
374 assert_kind_of TMail::Mail, mail
375 assert [mail.bcc, mail.cc].flatten.include?(User.find(3).mail)
375 assert [mail.bcc, mail.cc].flatten.include?(User.find(3).mail)
376 end
376 end
377
377
378 def test_post_should_preserve_fields_values_on_validation_failure
378 def test_post_should_preserve_fields_values_on_validation_failure
379 @request.session[:user_id] = 2
379 @request.session[:user_id] = 2
380 post :new, :project_id => 1,
380 post :new, :project_id => 1,
381 :issue => {:tracker_id => 1,
381 :issue => {:tracker_id => 1,
382 # empty subject
382 # empty subject
383 :subject => '',
383 :subject => '',
384 :description => 'This is a description',
384 :description => 'This is a description',
385 :priority_id => 6,
385 :priority_id => 6,
386 :custom_field_values => {'1' => 'Oracle', '2' => 'Value for field 2'}}
386 :custom_field_values => {'1' => 'Oracle', '2' => 'Value for field 2'}}
387 assert_response :success
387 assert_response :success
388 assert_template 'new'
388 assert_template 'new'
389
389
390 assert_tag :textarea, :attributes => { :name => 'issue[description]' },
390 assert_tag :textarea, :attributes => { :name => 'issue[description]' },
391 :content => 'This is a description'
391 :content => 'This is a description'
392 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
392 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
393 :child => { :tag => 'option', :attributes => { :selected => 'selected',
393 :child => { :tag => 'option', :attributes => { :selected => 'selected',
394 :value => '6' },
394 :value => '6' },
395 :content => 'High' }
395 :content => 'High' }
396 # Custom fields
396 # Custom fields
397 assert_tag :select, :attributes => { :name => 'issue[custom_field_values][1]' },
397 assert_tag :select, :attributes => { :name => 'issue[custom_field_values][1]' },
398 :child => { :tag => 'option', :attributes => { :selected => 'selected',
398 :child => { :tag => 'option', :attributes => { :selected => 'selected',
399 :value => 'Oracle' },
399 :value => 'Oracle' },
400 :content => 'Oracle' }
400 :content => 'Oracle' }
401 assert_tag :input, :attributes => { :name => 'issue[custom_field_values][2]',
401 assert_tag :input, :attributes => { :name => 'issue[custom_field_values][2]',
402 :value => 'Value for field 2'}
402 :value => 'Value for field 2'}
403 end
403 end
404
404
405 def test_copy_issue
405 def test_copy_issue
406 @request.session[:user_id] = 2
406 @request.session[:user_id] = 2
407 get :new, :project_id => 1, :copy_from => 1
407 get :new, :project_id => 1, :copy_from => 1
408 assert_template 'new'
408 assert_template 'new'
409 assert_not_nil assigns(:issue)
409 assert_not_nil assigns(:issue)
410 orig = Issue.find(1)
410 orig = Issue.find(1)
411 assert_equal orig.subject, assigns(:issue).subject
411 assert_equal orig.subject, assigns(:issue).subject
412 end
412 end
413
413
414 def test_get_edit
414 def test_get_edit
415 @request.session[:user_id] = 2
415 @request.session[:user_id] = 2
416 get :edit, :id => 1
416 get :edit, :id => 1
417 assert_response :success
417 assert_response :success
418 assert_template 'edit'
418 assert_template 'edit'
419 assert_not_nil assigns(:issue)
419 assert_not_nil assigns(:issue)
420 assert_equal Issue.find(1), assigns(:issue)
420 assert_equal Issue.find(1), assigns(:issue)
421 end
421 end
422
422
423 def test_get_edit_with_params
423 def test_get_edit_with_params
424 @request.session[:user_id] = 2
424 @request.session[:user_id] = 2
425 get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 }
425 get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 }
426 assert_response :success
426 assert_response :success
427 assert_template 'edit'
427 assert_template 'edit'
428
428
429 issue = assigns(:issue)
429 issue = assigns(:issue)
430 assert_not_nil issue
430 assert_not_nil issue
431
431
432 assert_equal 5, issue.status_id
432 assert_equal 5, issue.status_id
433 assert_tag :select, :attributes => { :name => 'issue[status_id]' },
433 assert_tag :select, :attributes => { :name => 'issue[status_id]' },
434 :child => { :tag => 'option',
434 :child => { :tag => 'option',
435 :content => 'Closed',
435 :content => 'Closed',
436 :attributes => { :selected => 'selected' } }
436 :attributes => { :selected => 'selected' } }
437
437
438 assert_equal 7, issue.priority_id
438 assert_equal 7, issue.priority_id
439 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
439 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
440 :child => { :tag => 'option',
440 :child => { :tag => 'option',
441 :content => 'Urgent',
441 :content => 'Urgent',
442 :attributes => { :selected => 'selected' } }
442 :attributes => { :selected => 'selected' } }
443 end
443 end
444
444
445 def test_reply_to_issue
445 def test_reply_to_issue
446 @request.session[:user_id] = 2
446 @request.session[:user_id] = 2
447 get :reply, :id => 1
447 get :reply, :id => 1
448 assert_response :success
448 assert_response :success
449 assert_select_rjs :show, "update"
449 assert_select_rjs :show, "update"
450 end
450 end
451
451
452 def test_reply_to_note
452 def test_reply_to_note
453 @request.session[:user_id] = 2
453 @request.session[:user_id] = 2
454 get :reply, :id => 1, :journal_id => 2
454 get :reply, :id => 1, :journal_id => 2
455 assert_response :success
455 assert_response :success
456 assert_select_rjs :show, "update"
456 assert_select_rjs :show, "update"
457 end
457 end
458
458
459 def test_post_edit_without_custom_fields_param
459 def test_post_edit_without_custom_fields_param
460 @request.session[:user_id] = 2
460 @request.session[:user_id] = 2
461 ActionMailer::Base.deliveries.clear
461 ActionMailer::Base.deliveries.clear
462
462
463 issue = Issue.find(1)
463 issue = Issue.find(1)
464 assert_equal '125', issue.custom_value_for(2).value
464 assert_equal '125', issue.custom_value_for(2).value
465 old_subject = issue.subject
465 old_subject = issue.subject
466 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
466 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
467
467
468 assert_difference('Journal.count') do
468 assert_difference('Journal.count') do
469 assert_difference('JournalDetail.count', 2) do
469 assert_difference('JournalDetail.count', 2) do
470 post :edit, :id => 1, :issue => {:subject => new_subject,
470 post :edit, :id => 1, :issue => {:subject => new_subject,
471 :priority_id => '6',
471 :priority_id => '6',
472 :category_id => '1' # no change
472 :category_id => '1' # no change
473 }
473 }
474 end
474 end
475 end
475 end
476 assert_redirected_to 'issues/show/1'
476 assert_redirected_to 'issues/show/1'
477 issue.reload
477 issue.reload
478 assert_equal new_subject, issue.subject
478 assert_equal new_subject, issue.subject
479 # Make sure custom fields were not cleared
479 # Make sure custom fields were not cleared
480 assert_equal '125', issue.custom_value_for(2).value
480 assert_equal '125', issue.custom_value_for(2).value
481
481
482 mail = ActionMailer::Base.deliveries.last
482 mail = ActionMailer::Base.deliveries.last
483 assert_kind_of TMail::Mail, mail
483 assert_kind_of TMail::Mail, mail
484 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
484 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
485 assert mail.body.include?("Subject changed from #{old_subject} to #{new_subject}")
485 assert mail.body.include?("Subject changed from #{old_subject} to #{new_subject}")
486 end
486 end
487
487
488 def test_post_edit_with_custom_field_change
488 def test_post_edit_with_custom_field_change
489 @request.session[:user_id] = 2
489 @request.session[:user_id] = 2
490 issue = Issue.find(1)
490 issue = Issue.find(1)
491 assert_equal '125', issue.custom_value_for(2).value
491 assert_equal '125', issue.custom_value_for(2).value
492
492
493 assert_difference('Journal.count') do
493 assert_difference('Journal.count') do
494 assert_difference('JournalDetail.count', 3) do
494 assert_difference('JournalDetail.count', 3) do
495 post :edit, :id => 1, :issue => {:subject => 'Custom field change',
495 post :edit, :id => 1, :issue => {:subject => 'Custom field change',
496 :priority_id => '6',
496 :priority_id => '6',
497 :category_id => '1', # no change
497 :category_id => '1', # no change
498 :custom_field_values => { '2' => 'New custom value' }
498 :custom_field_values => { '2' => 'New custom value' }
499 }
499 }
500 end
500 end
501 end
501 end
502 assert_redirected_to 'issues/show/1'
502 assert_redirected_to 'issues/show/1'
503 issue.reload
503 issue.reload
504 assert_equal 'New custom value', issue.custom_value_for(2).value
504 assert_equal 'New custom value', issue.custom_value_for(2).value
505
505
506 mail = ActionMailer::Base.deliveries.last
506 mail = ActionMailer::Base.deliveries.last
507 assert_kind_of TMail::Mail, mail
507 assert_kind_of TMail::Mail, mail
508 assert mail.body.include?("Searchable field changed from 125 to New custom value")
508 assert mail.body.include?("Searchable field changed from 125 to New custom value")
509 end
509 end
510
510
511 def test_post_edit_with_status_and_assignee_change
511 def test_post_edit_with_status_and_assignee_change
512 issue = Issue.find(1)
512 issue = Issue.find(1)
513 assert_equal 1, issue.status_id
513 assert_equal 1, issue.status_id
514 @request.session[:user_id] = 2
514 @request.session[:user_id] = 2
515 assert_difference('TimeEntry.count', 0) do
515 assert_difference('TimeEntry.count', 0) do
516 post :edit,
516 post :edit,
517 :id => 1,
517 :id => 1,
518 :issue => { :status_id => 2, :assigned_to_id => 3 },
518 :issue => { :status_id => 2, :assigned_to_id => 3 },
519 :notes => 'Assigned to dlopper',
519 :notes => 'Assigned to dlopper',
520 :time_entry => { :hours => '', :comments => '', :activity_id => Enumeration.get_values('ACTI').first }
520 :time_entry => { :hours => '', :comments => '', :activity_id => Enumeration.get_values('ACTI').first }
521 end
521 end
522 assert_redirected_to 'issues/show/1'
522 assert_redirected_to 'issues/show/1'
523 issue.reload
523 issue.reload
524 assert_equal 2, issue.status_id
524 assert_equal 2, issue.status_id
525 j = issue.journals.find(:first, :order => 'id DESC')
525 j = issue.journals.find(:first, :order => 'id DESC')
526 assert_equal 'Assigned to dlopper', j.notes
526 assert_equal 'Assigned to dlopper', j.notes
527 assert_equal 2, j.details.size
527 assert_equal 2, j.details.size
528
528
529 mail = ActionMailer::Base.deliveries.last
529 mail = ActionMailer::Base.deliveries.last
530 assert mail.body.include?("Status changed from New to Assigned")
530 assert mail.body.include?("Status changed from New to Assigned")
531 end
531 end
532
532
533 def test_post_edit_with_note_only
533 def test_post_edit_with_note_only
534 notes = 'Note added by IssuesControllerTest#test_update_with_note_only'
534 notes = 'Note added by IssuesControllerTest#test_update_with_note_only'
535 # anonymous user
535 # anonymous user
536 post :edit,
536 post :edit,
537 :id => 1,
537 :id => 1,
538 :notes => notes
538 :notes => notes
539 assert_redirected_to 'issues/show/1'
539 assert_redirected_to 'issues/show/1'
540 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
540 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
541 assert_equal notes, j.notes
541 assert_equal notes, j.notes
542 assert_equal 0, j.details.size
542 assert_equal 0, j.details.size
543 assert_equal User.anonymous, j.user
543 assert_equal User.anonymous, j.user
544
544
545 mail = ActionMailer::Base.deliveries.last
545 mail = ActionMailer::Base.deliveries.last
546 assert mail.body.include?(notes)
546 assert mail.body.include?(notes)
547 end
547 end
548
548
549 def test_post_edit_with_note_and_spent_time
549 def test_post_edit_with_note_and_spent_time
550 @request.session[:user_id] = 2
550 @request.session[:user_id] = 2
551 spent_hours_before = Issue.find(1).spent_hours
551 spent_hours_before = Issue.find(1).spent_hours
552 assert_difference('TimeEntry.count') do
552 assert_difference('TimeEntry.count') do
553 post :edit,
553 post :edit,
554 :id => 1,
554 :id => 1,
555 :notes => '2.5 hours added',
555 :notes => '2.5 hours added',
556 :time_entry => { :hours => '2.5', :comments => '', :activity_id => Enumeration.get_values('ACTI').first }
556 :time_entry => { :hours => '2.5', :comments => '', :activity_id => Enumeration.get_values('ACTI').first }
557 end
557 end
558 assert_redirected_to 'issues/show/1'
558 assert_redirected_to 'issues/show/1'
559
559
560 issue = Issue.find(1)
560 issue = Issue.find(1)
561
561
562 j = issue.journals.find(:first, :order => 'id DESC')
562 j = issue.journals.find(:first, :order => 'id DESC')
563 assert_equal '2.5 hours added', j.notes
563 assert_equal '2.5 hours added', j.notes
564 assert_equal 0, j.details.size
564 assert_equal 0, j.details.size
565
565
566 t = issue.time_entries.find(:first, :order => 'id DESC')
566 t = issue.time_entries.find(:first, :order => 'id DESC')
567 assert_not_nil t
567 assert_not_nil t
568 assert_equal 2.5, t.hours
568 assert_equal 2.5, t.hours
569 assert_equal spent_hours_before + 2.5, issue.spent_hours
569 assert_equal spent_hours_before + 2.5, issue.spent_hours
570 end
570 end
571
571
572 def test_post_edit_with_attachment_only
572 def test_post_edit_with_attachment_only
573 set_tmp_attachments_directory
573 set_tmp_attachments_directory
574
574
575 # Delete all fixtured journals, a race condition can occur causing the wrong
575 # Delete all fixtured journals, a race condition can occur causing the wrong
576 # journal to get fetched in the next find.
576 # journal to get fetched in the next find.
577 Journal.delete_all
577 Journal.delete_all
578
578
579 # anonymous user
579 # anonymous user
580 post :edit,
580 post :edit,
581 :id => 1,
581 :id => 1,
582 :notes => '',
582 :notes => '',
583 :attachments => {'1' => {'file' => test_uploaded_file('testfile.txt', 'text/plain')}}
583 :attachments => {'1' => {'file' => test_uploaded_file('testfile.txt', 'text/plain')}}
584 assert_redirected_to 'issues/show/1'
584 assert_redirected_to 'issues/show/1'
585 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
585 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
586 assert j.notes.blank?
586 assert j.notes.blank?
587 assert_equal 1, j.details.size
587 assert_equal 1, j.details.size
588 assert_equal 'testfile.txt', j.details.first.value
588 assert_equal 'testfile.txt', j.details.first.value
589 assert_equal User.anonymous, j.user
589 assert_equal User.anonymous, j.user
590
590
591 mail = ActionMailer::Base.deliveries.last
591 mail = ActionMailer::Base.deliveries.last
592 assert mail.body.include?('testfile.txt')
592 assert mail.body.include?('testfile.txt')
593 end
593 end
594
594
595 def test_post_edit_with_no_change
595 def test_post_edit_with_no_change
596 issue = Issue.find(1)
596 issue = Issue.find(1)
597 issue.journals.clear
597 issue.journals.clear
598 ActionMailer::Base.deliveries.clear
598 ActionMailer::Base.deliveries.clear
599
599
600 post :edit,
600 post :edit,
601 :id => 1,
601 :id => 1,
602 :notes => ''
602 :notes => ''
603 assert_redirected_to 'issues/show/1'
603 assert_redirected_to 'issues/show/1'
604
604
605 issue.reload
605 issue.reload
606 assert issue.journals.empty?
606 assert issue.journals.empty?
607 # No email should be sent
607 # No email should be sent
608 assert ActionMailer::Base.deliveries.empty?
608 assert ActionMailer::Base.deliveries.empty?
609 end
609 end
610
610
611 def test_post_edit_with_invalid_spent_time
611 def test_post_edit_with_invalid_spent_time
612 @request.session[:user_id] = 2
612 @request.session[:user_id] = 2
613 notes = 'Note added by IssuesControllerTest#test_post_edit_with_invalid_spent_time'
613 notes = 'Note added by IssuesControllerTest#test_post_edit_with_invalid_spent_time'
614
614
615 assert_no_difference('Journal.count') do
615 assert_no_difference('Journal.count') do
616 post :edit,
616 post :edit,
617 :id => 1,
617 :id => 1,
618 :notes => notes,
618 :notes => notes,
619 :time_entry => {"comments"=>"", "activity_id"=>"", "hours"=>"2z"}
619 :time_entry => {"comments"=>"", "activity_id"=>"", "hours"=>"2z"}
620 end
620 end
621 assert_response :success
621 assert_response :success
622 assert_template 'edit'
622 assert_template 'edit'
623
623
624 assert_tag :textarea, :attributes => { :name => 'notes' },
624 assert_tag :textarea, :attributes => { :name => 'notes' },
625 :content => notes
625 :content => notes
626 assert_tag :input, :attributes => { :name => 'time_entry[hours]', :value => "2z" }
626 assert_tag :input, :attributes => { :name => 'time_entry[hours]', :value => "2z" }
627 end
627 end
628
628
629 def test_bulk_edit
629 def test_bulk_edit
630 @request.session[:user_id] = 2
630 @request.session[:user_id] = 2
631 # update issues priority
631 # update issues priority
632 post :bulk_edit, :ids => [1, 2], :priority_id => 7, :notes => 'Bulk editing', :assigned_to_id => ''
632 post :bulk_edit, :ids => [1, 2], :priority_id => 7, :notes => 'Bulk editing', :assigned_to_id => ''
633 assert_response 302
633 assert_response 302
634 # check that the issues were updated
634 # check that the issues were updated
635 assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id}
635 assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id}
636 assert_equal 'Bulk editing', Issue.find(1).journals.find(:first, :order => 'created_on DESC').notes
636 assert_equal 'Bulk editing', Issue.find(1).journals.find(:first, :order => 'created_on DESC').notes
637 end
637 end
638
638
639 def test_bulk_unassign
639 def test_bulk_unassign
640 assert_not_nil Issue.find(2).assigned_to
640 assert_not_nil Issue.find(2).assigned_to
641 @request.session[:user_id] = 2
641 @request.session[:user_id] = 2
642 # unassign issues
642 # unassign issues
643 post :bulk_edit, :ids => [1, 2], :notes => 'Bulk unassigning', :assigned_to_id => 'none'
643 post :bulk_edit, :ids => [1, 2], :notes => 'Bulk unassigning', :assigned_to_id => 'none'
644 assert_response 302
644 assert_response 302
645 # check that the issues were updated
645 # check that the issues were updated
646 assert_nil Issue.find(2).assigned_to
646 assert_nil Issue.find(2).assigned_to
647 end
647 end
648
648
649 def test_move_one_issue_to_another_project
649 def test_move_one_issue_to_another_project
650 @request.session[:user_id] = 1
650 @request.session[:user_id] = 1
651 post :move, :id => 1, :new_project_id => 2
651 post :move, :id => 1, :new_project_id => 2
652 assert_redirected_to 'projects/ecookbook/issues'
652 assert_redirected_to 'projects/ecookbook/issues'
653 assert_equal 2, Issue.find(1).project_id
653 assert_equal 2, Issue.find(1).project_id
654 end
654 end
655
655
656 def test_bulk_move_to_another_project
656 def test_bulk_move_to_another_project
657 @request.session[:user_id] = 1
657 @request.session[:user_id] = 1
658 post :move, :ids => [1, 2], :new_project_id => 2
658 post :move, :ids => [1, 2], :new_project_id => 2
659 assert_redirected_to 'projects/ecookbook/issues'
659 assert_redirected_to 'projects/ecookbook/issues'
660 # Issues moved to project 2
660 # Issues moved to project 2
661 assert_equal 2, Issue.find(1).project_id
661 assert_equal 2, Issue.find(1).project_id
662 assert_equal 2, Issue.find(2).project_id
662 assert_equal 2, Issue.find(2).project_id
663 # No tracker change
663 # No tracker change
664 assert_equal 1, Issue.find(1).tracker_id
664 assert_equal 1, Issue.find(1).tracker_id
665 assert_equal 2, Issue.find(2).tracker_id
665 assert_equal 2, Issue.find(2).tracker_id
666 end
666 end
667
667
668 def test_bulk_move_to_another_tracker
668 def test_bulk_move_to_another_tracker
669 @request.session[:user_id] = 1
669 @request.session[:user_id] = 1
670 post :move, :ids => [1, 2], :new_tracker_id => 2
670 post :move, :ids => [1, 2], :new_tracker_id => 2
671 assert_redirected_to 'projects/ecookbook/issues'
671 assert_redirected_to 'projects/ecookbook/issues'
672 assert_equal 2, Issue.find(1).tracker_id
672 assert_equal 2, Issue.find(1).tracker_id
673 assert_equal 2, Issue.find(2).tracker_id
673 assert_equal 2, Issue.find(2).tracker_id
674 end
674 end
675
676 def test_bulk_copy_to_another_project
677 @request.session[:user_id] = 1
678 assert_difference 'Issue.count', 2 do
679 assert_no_difference 'Project.find(1).issues.count' do
680 post :move, :ids => [1, 2], :new_project_id => 2, :copy_options => {:copy => '1'}
681 end
682 end
683 assert_redirected_to 'projects/ecookbook/issues'
684 end
675
685
676 def test_context_menu_one_issue
686 def test_context_menu_one_issue
677 @request.session[:user_id] = 2
687 @request.session[:user_id] = 2
678 get :context_menu, :ids => [1]
688 get :context_menu, :ids => [1]
679 assert_response :success
689 assert_response :success
680 assert_template 'context_menu'
690 assert_template 'context_menu'
681 assert_tag :tag => 'a', :content => 'Edit',
691 assert_tag :tag => 'a', :content => 'Edit',
682 :attributes => { :href => '/issues/edit/1',
692 :attributes => { :href => '/issues/edit/1',
683 :class => 'icon-edit' }
693 :class => 'icon-edit' }
684 assert_tag :tag => 'a', :content => 'Closed',
694 assert_tag :tag => 'a', :content => 'Closed',
685 :attributes => { :href => '/issues/edit/1?issue%5Bstatus_id%5D=5',
695 :attributes => { :href => '/issues/edit/1?issue%5Bstatus_id%5D=5',
686 :class => '' }
696 :class => '' }
687 assert_tag :tag => 'a', :content => 'Immediate',
697 assert_tag :tag => 'a', :content => 'Immediate',
688 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;priority_id=8',
698 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;priority_id=8',
689 :class => '' }
699 :class => '' }
690 assert_tag :tag => 'a', :content => 'Dave Lopper',
700 assert_tag :tag => 'a', :content => 'Dave Lopper',
691 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1',
701 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1',
692 :class => '' }
702 :class => '' }
693 assert_tag :tag => 'a', :content => 'Copy',
703 assert_tag :tag => 'a', :content => 'Copy',
694 :attributes => { :href => '/projects/ecookbook/issues/new?copy_from=1',
704 :attributes => { :href => '/projects/ecookbook/issues/new?copy_from=1',
695 :class => 'icon-copy' }
705 :class => 'icon-copy' }
696 assert_tag :tag => 'a', :content => 'Move',
706 assert_tag :tag => 'a', :content => 'Move',
697 :attributes => { :href => '/issues/move?ids%5B%5D=1',
707 :attributes => { :href => '/issues/move?ids%5B%5D=1',
698 :class => 'icon-move' }
708 :class => 'icon-move' }
699 assert_tag :tag => 'a', :content => 'Delete',
709 assert_tag :tag => 'a', :content => 'Delete',
700 :attributes => { :href => '/issues/destroy?ids%5B%5D=1',
710 :attributes => { :href => '/issues/destroy?ids%5B%5D=1',
701 :class => 'icon-del' }
711 :class => 'icon-del' }
702 end
712 end
703
713
704 def test_context_menu_one_issue_by_anonymous
714 def test_context_menu_one_issue_by_anonymous
705 get :context_menu, :ids => [1]
715 get :context_menu, :ids => [1]
706 assert_response :success
716 assert_response :success
707 assert_template 'context_menu'
717 assert_template 'context_menu'
708 assert_tag :tag => 'a', :content => 'Delete',
718 assert_tag :tag => 'a', :content => 'Delete',
709 :attributes => { :href => '#',
719 :attributes => { :href => '#',
710 :class => 'icon-del disabled' }
720 :class => 'icon-del disabled' }
711 end
721 end
712
722
713 def test_context_menu_multiple_issues_of_same_project
723 def test_context_menu_multiple_issues_of_same_project
714 @request.session[:user_id] = 2
724 @request.session[:user_id] = 2
715 get :context_menu, :ids => [1, 2]
725 get :context_menu, :ids => [1, 2]
716 assert_response :success
726 assert_response :success
717 assert_template 'context_menu'
727 assert_template 'context_menu'
718 assert_tag :tag => 'a', :content => 'Edit',
728 assert_tag :tag => 'a', :content => 'Edit',
719 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2',
729 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2',
720 :class => 'icon-edit' }
730 :class => 'icon-edit' }
721 assert_tag :tag => 'a', :content => 'Immediate',
731 assert_tag :tag => 'a', :content => 'Immediate',
722 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2&amp;priority_id=8',
732 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2&amp;priority_id=8',
723 :class => '' }
733 :class => '' }
724 assert_tag :tag => 'a', :content => 'Dave Lopper',
734 assert_tag :tag => 'a', :content => 'Dave Lopper',
725 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1&amp;ids%5B%5D=2',
735 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1&amp;ids%5B%5D=2',
726 :class => '' }
736 :class => '' }
727 assert_tag :tag => 'a', :content => 'Move',
737 assert_tag :tag => 'a', :content => 'Move',
728 :attributes => { :href => '/issues/move?ids%5B%5D=1&amp;ids%5B%5D=2',
738 :attributes => { :href => '/issues/move?ids%5B%5D=1&amp;ids%5B%5D=2',
729 :class => 'icon-move' }
739 :class => 'icon-move' }
730 assert_tag :tag => 'a', :content => 'Delete',
740 assert_tag :tag => 'a', :content => 'Delete',
731 :attributes => { :href => '/issues/destroy?ids%5B%5D=1&amp;ids%5B%5D=2',
741 :attributes => { :href => '/issues/destroy?ids%5B%5D=1&amp;ids%5B%5D=2',
732 :class => 'icon-del' }
742 :class => 'icon-del' }
733 end
743 end
734
744
735 def test_context_menu_multiple_issues_of_different_project
745 def test_context_menu_multiple_issues_of_different_project
736 @request.session[:user_id] = 2
746 @request.session[:user_id] = 2
737 get :context_menu, :ids => [1, 2, 4]
747 get :context_menu, :ids => [1, 2, 4]
738 assert_response :success
748 assert_response :success
739 assert_template 'context_menu'
749 assert_template 'context_menu'
740 assert_tag :tag => 'a', :content => 'Delete',
750 assert_tag :tag => 'a', :content => 'Delete',
741 :attributes => { :href => '#',
751 :attributes => { :href => '#',
742 :class => 'icon-del disabled' }
752 :class => 'icon-del disabled' }
743 end
753 end
744
754
745 def test_destroy_issue_with_no_time_entries
755 def test_destroy_issue_with_no_time_entries
746 assert_nil TimeEntry.find_by_issue_id(2)
756 assert_nil TimeEntry.find_by_issue_id(2)
747 @request.session[:user_id] = 2
757 @request.session[:user_id] = 2
748 post :destroy, :id => 2
758 post :destroy, :id => 2
749 assert_redirected_to 'projects/ecookbook/issues'
759 assert_redirected_to 'projects/ecookbook/issues'
750 assert_nil Issue.find_by_id(2)
760 assert_nil Issue.find_by_id(2)
751 end
761 end
752
762
753 def test_destroy_issues_with_time_entries
763 def test_destroy_issues_with_time_entries
754 @request.session[:user_id] = 2
764 @request.session[:user_id] = 2
755 post :destroy, :ids => [1, 3]
765 post :destroy, :ids => [1, 3]
756 assert_response :success
766 assert_response :success
757 assert_template 'destroy'
767 assert_template 'destroy'
758 assert_not_nil assigns(:hours)
768 assert_not_nil assigns(:hours)
759 assert Issue.find_by_id(1) && Issue.find_by_id(3)
769 assert Issue.find_by_id(1) && Issue.find_by_id(3)
760 end
770 end
761
771
762 def test_destroy_issues_and_destroy_time_entries
772 def test_destroy_issues_and_destroy_time_entries
763 @request.session[:user_id] = 2
773 @request.session[:user_id] = 2
764 post :destroy, :ids => [1, 3], :todo => 'destroy'
774 post :destroy, :ids => [1, 3], :todo => 'destroy'
765 assert_redirected_to 'projects/ecookbook/issues'
775 assert_redirected_to 'projects/ecookbook/issues'
766 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
776 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
767 assert_nil TimeEntry.find_by_id([1, 2])
777 assert_nil TimeEntry.find_by_id([1, 2])
768 end
778 end
769
779
770 def test_destroy_issues_and_assign_time_entries_to_project
780 def test_destroy_issues_and_assign_time_entries_to_project
771 @request.session[:user_id] = 2
781 @request.session[:user_id] = 2
772 post :destroy, :ids => [1, 3], :todo => 'nullify'
782 post :destroy, :ids => [1, 3], :todo => 'nullify'
773 assert_redirected_to 'projects/ecookbook/issues'
783 assert_redirected_to 'projects/ecookbook/issues'
774 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
784 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
775 assert_nil TimeEntry.find(1).issue_id
785 assert_nil TimeEntry.find(1).issue_id
776 assert_nil TimeEntry.find(2).issue_id
786 assert_nil TimeEntry.find(2).issue_id
777 end
787 end
778
788
779 def test_destroy_issues_and_reassign_time_entries_to_another_issue
789 def test_destroy_issues_and_reassign_time_entries_to_another_issue
780 @request.session[:user_id] = 2
790 @request.session[:user_id] = 2
781 post :destroy, :ids => [1, 3], :todo => 'reassign', :reassign_to_id => 2
791 post :destroy, :ids => [1, 3], :todo => 'reassign', :reassign_to_id => 2
782 assert_redirected_to 'projects/ecookbook/issues'
792 assert_redirected_to 'projects/ecookbook/issues'
783 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
793 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
784 assert_equal 2, TimeEntry.find(1).issue_id
794 assert_equal 2, TimeEntry.find(1).issue_id
785 assert_equal 2, TimeEntry.find(2).issue_id
795 assert_equal 2, TimeEntry.find(2).issue_id
786 end
796 end
787 end
797 end
@@ -1,206 +1,230
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 < Test::Unit::TestCase
20 class IssueTest < Test::Unit::TestCase
21 fixtures :projects, :users, :members,
21 fixtures :projects, :users, :members,
22 :trackers, :projects_trackers,
22 :trackers, :projects_trackers,
23 :issue_statuses, :issue_categories,
23 :issue_statuses, :issue_categories,
24 :enumerations,
24 :enumerations,
25 :issues,
25 :issues,
26 :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values,
26 :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values,
27 :time_entries
27 :time_entries
28
28
29 def test_create
29 def test_create
30 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => Enumeration.get_values('IPRI').first, :subject => 'test_create', :description => 'IssueTest#test_create', :estimated_hours => '1:30')
30 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => Enumeration.get_values('IPRI').first, :subject => 'test_create', :description => 'IssueTest#test_create', :estimated_hours => '1:30')
31 assert issue.save
31 assert issue.save
32 issue.reload
32 issue.reload
33 assert_equal 1.5, issue.estimated_hours
33 assert_equal 1.5, issue.estimated_hours
34 end
34 end
35
35
36 def test_create_minimal
36 def test_create_minimal
37 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => Enumeration.get_values('IPRI').first, :subject => 'test_create')
37 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => Enumeration.get_values('IPRI').first, :subject => 'test_create')
38 assert issue.save
38 assert issue.save
39 assert issue.description.nil?
39 assert issue.description.nil?
40 end
40 end
41
41
42 def test_create_with_required_custom_field
42 def test_create_with_required_custom_field
43 field = IssueCustomField.find_by_name('Database')
43 field = IssueCustomField.find_by_name('Database')
44 field.update_attribute(:is_required, true)
44 field.update_attribute(:is_required, true)
45
45
46 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')
46 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 assert issue.available_custom_fields.include?(field)
47 assert issue.available_custom_fields.include?(field)
48 # No value for the custom field
48 # No value for the custom field
49 assert !issue.save
49 assert !issue.save
50 assert_equal 'activerecord_error_invalid', issue.errors.on(:custom_values)
50 assert_equal 'activerecord_error_invalid', issue.errors.on(:custom_values)
51 # Blank value
51 # Blank value
52 issue.custom_field_values = { field.id => '' }
52 issue.custom_field_values = { field.id => '' }
53 assert !issue.save
53 assert !issue.save
54 assert_equal 'activerecord_error_invalid', issue.errors.on(:custom_values)
54 assert_equal 'activerecord_error_invalid', issue.errors.on(:custom_values)
55 # Invalid value
55 # Invalid value
56 issue.custom_field_values = { field.id => 'SQLServer' }
56 issue.custom_field_values = { field.id => 'SQLServer' }
57 assert !issue.save
57 assert !issue.save
58 assert_equal 'activerecord_error_invalid', issue.errors.on(:custom_values)
58 assert_equal 'activerecord_error_invalid', issue.errors.on(:custom_values)
59 # Valid value
59 # Valid value
60 issue.custom_field_values = { field.id => 'PostgreSQL' }
60 issue.custom_field_values = { field.id => 'PostgreSQL' }
61 assert issue.save
61 assert issue.save
62 issue.reload
62 issue.reload
63 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
63 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
64 end
64 end
65
65
66 def test_update_issue_with_required_custom_field
66 def test_update_issue_with_required_custom_field
67 field = IssueCustomField.find_by_name('Database')
67 field = IssueCustomField.find_by_name('Database')
68 field.update_attribute(:is_required, true)
68 field.update_attribute(:is_required, true)
69
69
70 issue = Issue.find(1)
70 issue = Issue.find(1)
71 assert_nil issue.custom_value_for(field)
71 assert_nil issue.custom_value_for(field)
72 assert issue.available_custom_fields.include?(field)
72 assert issue.available_custom_fields.include?(field)
73 # No change to custom values, issue can be saved
73 # No change to custom values, issue can be saved
74 assert issue.save
74 assert issue.save
75 # Blank value
75 # Blank value
76 issue.custom_field_values = { field.id => '' }
76 issue.custom_field_values = { field.id => '' }
77 assert !issue.save
77 assert !issue.save
78 # Valid value
78 # Valid value
79 issue.custom_field_values = { field.id => 'PostgreSQL' }
79 issue.custom_field_values = { field.id => 'PostgreSQL' }
80 assert issue.save
80 assert issue.save
81 issue.reload
81 issue.reload
82 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
82 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
83 end
83 end
84
84
85 def test_should_not_update_attributes_if_custom_fields_validation_fails
85 def test_should_not_update_attributes_if_custom_fields_validation_fails
86 issue = Issue.find(1)
86 issue = Issue.find(1)
87 field = IssueCustomField.find_by_name('Database')
87 field = IssueCustomField.find_by_name('Database')
88 assert issue.available_custom_fields.include?(field)
88 assert issue.available_custom_fields.include?(field)
89
89
90 issue.custom_field_values = { field.id => 'Invalid' }
90 issue.custom_field_values = { field.id => 'Invalid' }
91 issue.subject = 'Should be not be saved'
91 issue.subject = 'Should be not be saved'
92 assert !issue.save
92 assert !issue.save
93
93
94 issue.reload
94 issue.reload
95 assert_equal "Can't print recipes", issue.subject
95 assert_equal "Can't print recipes", issue.subject
96 end
96 end
97
97
98 def test_should_not_recreate_custom_values_objects_on_update
98 def test_should_not_recreate_custom_values_objects_on_update
99 field = IssueCustomField.find_by_name('Database')
99 field = IssueCustomField.find_by_name('Database')
100
100
101 issue = Issue.find(1)
101 issue = Issue.find(1)
102 issue.custom_field_values = { field.id => 'PostgreSQL' }
102 issue.custom_field_values = { field.id => 'PostgreSQL' }
103 assert issue.save
103 assert issue.save
104 custom_value = issue.custom_value_for(field)
104 custom_value = issue.custom_value_for(field)
105 issue.reload
105 issue.reload
106 issue.custom_field_values = { field.id => 'MySQL' }
106 issue.custom_field_values = { field.id => 'MySQL' }
107 assert issue.save
107 assert issue.save
108 issue.reload
108 issue.reload
109 assert_equal custom_value.id, issue.custom_value_for(field).id
109 assert_equal custom_value.id, issue.custom_value_for(field).id
110 end
110 end
111
111
112 def test_category_based_assignment
112 def test_category_based_assignment
113 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => Enumeration.get_values('IPRI').first, :subject => 'Assignment test', :description => 'Assignment test', :category_id => 1)
113 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => Enumeration.get_values('IPRI').first, :subject => 'Assignment test', :description => 'Assignment test', :category_id => 1)
114 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
114 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
115 end
115 end
116
116
117 def test_copy
117 def test_copy
118 issue = Issue.new.copy_from(1)
118 issue = Issue.new.copy_from(1)
119 assert issue.save
119 assert issue.save
120 issue.reload
120 issue.reload
121 orig = Issue.find(1)
121 orig = Issue.find(1)
122 assert_equal orig.subject, issue.subject
122 assert_equal orig.subject, issue.subject
123 assert_equal orig.tracker, issue.tracker
123 assert_equal orig.tracker, issue.tracker
124 assert_equal orig.custom_values.first.value, issue.custom_values.first.value
124 assert_equal orig.custom_values.first.value, issue.custom_values.first.value
125 end
125 end
126
126
127 def test_should_close_duplicates
127 def test_should_close_duplicates
128 # Create 3 issues
128 # Create 3 issues
129 issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => Enumeration.get_values('IPRI').first, :subject => 'Duplicates test', :description => 'Duplicates test')
129 issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => Enumeration.get_values('IPRI').first, :subject => 'Duplicates test', :description => 'Duplicates test')
130 assert issue1.save
130 assert issue1.save
131 issue2 = issue1.clone
131 issue2 = issue1.clone
132 assert issue2.save
132 assert issue2.save
133 issue3 = issue1.clone
133 issue3 = issue1.clone
134 assert issue3.save
134 assert issue3.save
135
135
136 # 2 is a dupe of 1
136 # 2 is a dupe of 1
137 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
137 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
138 # And 3 is a dupe of 2
138 # And 3 is a dupe of 2
139 IssueRelation.create(:issue_from => issue3, :issue_to => issue2, :relation_type => IssueRelation::TYPE_DUPLICATES)
139 IssueRelation.create(:issue_from => issue3, :issue_to => issue2, :relation_type => IssueRelation::TYPE_DUPLICATES)
140 # And 3 is a dupe of 1 (circular duplicates)
140 # And 3 is a dupe of 1 (circular duplicates)
141 IssueRelation.create(:issue_from => issue3, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
141 IssueRelation.create(:issue_from => issue3, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
142
142
143 assert issue1.reload.duplicates.include?(issue2)
143 assert issue1.reload.duplicates.include?(issue2)
144
144
145 # Closing issue 1
145 # Closing issue 1
146 issue1.init_journal(User.find(:first), "Closing issue1")
146 issue1.init_journal(User.find(:first), "Closing issue1")
147 issue1.status = IssueStatus.find :first, :conditions => {:is_closed => true}
147 issue1.status = IssueStatus.find :first, :conditions => {:is_closed => true}
148 assert issue1.save
148 assert issue1.save
149 # 2 and 3 should be also closed
149 # 2 and 3 should be also closed
150 assert issue2.reload.closed?
150 assert issue2.reload.closed?
151 assert issue3.reload.closed?
151 assert issue3.reload.closed?
152 end
152 end
153
153
154 def test_should_not_close_duplicated_issue
154 def test_should_not_close_duplicated_issue
155 # Create 3 issues
155 # Create 3 issues
156 issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => Enumeration.get_values('IPRI').first, :subject => 'Duplicates test', :description => 'Duplicates test')
156 issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => Enumeration.get_values('IPRI').first, :subject => 'Duplicates test', :description => 'Duplicates test')
157 assert issue1.save
157 assert issue1.save
158 issue2 = issue1.clone
158 issue2 = issue1.clone
159 assert issue2.save
159 assert issue2.save
160
160
161 # 2 is a dupe of 1
161 # 2 is a dupe of 1
162 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
162 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
163 # 2 is a dup of 1 but 1 is not a duplicate of 2
163 # 2 is a dup of 1 but 1 is not a duplicate of 2
164 assert !issue2.reload.duplicates.include?(issue1)
164 assert !issue2.reload.duplicates.include?(issue1)
165
165
166 # Closing issue 2
166 # Closing issue 2
167 issue2.init_journal(User.find(:first), "Closing issue2")
167 issue2.init_journal(User.find(:first), "Closing issue2")
168 issue2.status = IssueStatus.find :first, :conditions => {:is_closed => true}
168 issue2.status = IssueStatus.find :first, :conditions => {:is_closed => true}
169 assert issue2.save
169 assert issue2.save
170 # 1 should not be also closed
170 # 1 should not be also closed
171 assert !issue1.reload.closed?
171 assert !issue1.reload.closed?
172 end
172 end
173
173
174 def test_move_to_another_project_with_same_category
174 def test_move_to_another_project_with_same_category
175 issue = Issue.find(1)
175 issue = Issue.find(1)
176 assert issue.move_to(Project.find(2))
176 assert issue.move_to(Project.find(2))
177 issue.reload
177 issue.reload
178 assert_equal 2, issue.project_id
178 assert_equal 2, issue.project_id
179 # Category changes
179 # Category changes
180 assert_equal 4, issue.category_id
180 assert_equal 4, issue.category_id
181 # Make sure time entries were move to the target project
181 # Make sure time entries were move to the target project
182 assert_equal 2, issue.time_entries.first.project_id
182 assert_equal 2, issue.time_entries.first.project_id
183 end
183 end
184
184
185 def test_move_to_another_project_without_same_category
185 def test_move_to_another_project_without_same_category
186 issue = Issue.find(2)
186 issue = Issue.find(2)
187 assert issue.move_to(Project.find(2))
187 assert issue.move_to(Project.find(2))
188 issue.reload
188 issue.reload
189 assert_equal 2, issue.project_id
189 assert_equal 2, issue.project_id
190 # Category cleared
190 # Category cleared
191 assert_nil issue.category_id
191 assert_nil issue.category_id
192 end
192 end
193
193
194 def test_copy_to_the_same_project
195 issue = Issue.find(1)
196 copy = nil
197 assert_difference 'Issue.count' do
198 copy = issue.move_to(issue.project, nil, :copy => true)
199 end
200 assert_kind_of Issue, copy
201 assert_equal issue.project, copy.project
202 assert_equal "125", copy.custom_value_for(2).value
203 end
204
205 def test_copy_to_another_project_and_tracker
206 issue = Issue.find(1)
207 copy = nil
208 assert_difference 'Issue.count' do
209 copy = issue.move_to(Project.find(3), Tracker.find(2), :copy => true)
210 end
211 assert_kind_of Issue, copy
212 assert_equal Project.find(3), copy.project
213 assert_equal Tracker.find(2), copy.tracker
214 # Custom field #2 is not associated with target tracker
215 assert_nil copy.custom_value_for(2)
216 end
217
194 def test_issue_destroy
218 def test_issue_destroy
195 Issue.find(1).destroy
219 Issue.find(1).destroy
196 assert_nil Issue.find_by_id(1)
220 assert_nil Issue.find_by_id(1)
197 assert_nil TimeEntry.find_by_issue_id(1)
221 assert_nil TimeEntry.find_by_issue_id(1)
198 end
222 end
199
223
200 def test_overdue
224 def test_overdue
201 assert Issue.new(:due_date => 1.day.ago.to_date).overdue?
225 assert Issue.new(:due_date => 1.day.ago.to_date).overdue?
202 assert !Issue.new(:due_date => Date.today).overdue?
226 assert !Issue.new(:due_date => Date.today).overdue?
203 assert !Issue.new(:due_date => 1.day.from_now.to_date).overdue?
227 assert !Issue.new(:due_date => 1.day.from_now.to_date).overdue?
204 assert !Issue.new(:due_date => nil).overdue?
228 assert !Issue.new(:due_date => nil).overdue?
205 end
229 end
206 end
230 end
General Comments 0
You need to be logged in to leave comments. Login now