##// END OF EJS Templates
Fixed: PDF export of a issue list grouped by a custom field raises an error (#4600)....
Jean-Philippe Lang -
r3219:320c191f04d8
parent child
Show More
@@ -1,571 +1,572
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class IssuesController < ApplicationController
18 class IssuesController < ApplicationController
19 menu_item :new_issue, :only => :new
19 menu_item :new_issue, :only => :new
20 default_search_scope :issues
20 default_search_scope :issues
21
21
22 before_filter :find_issue, :only => [:show, :edit, :reply]
22 before_filter :find_issue, :only => [:show, :edit, :reply]
23 before_filter :find_issues, :only => [:bulk_edit, :move, :destroy]
23 before_filter :find_issues, :only => [:bulk_edit, :move, :destroy]
24 before_filter :find_project, :only => [:new, :update_form, :preview]
24 before_filter :find_project, :only => [:new, :update_form, :preview]
25 before_filter :authorize, :except => [:index, :changes, :gantt, :calendar, :preview, :context_menu]
25 before_filter :authorize, :except => [:index, :changes, :gantt, :calendar, :preview, :context_menu]
26 before_filter :find_optional_project, :only => [:index, :changes, :gantt, :calendar]
26 before_filter :find_optional_project, :only => [:index, :changes, :gantt, :calendar]
27 accept_key_auth :index, :show, :changes
27 accept_key_auth :index, :show, :changes
28
28
29 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
29 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
30
30
31 helper :journals
31 helper :journals
32 helper :projects
32 helper :projects
33 include ProjectsHelper
33 include ProjectsHelper
34 helper :custom_fields
34 helper :custom_fields
35 include CustomFieldsHelper
35 include CustomFieldsHelper
36 helper :issue_relations
36 helper :issue_relations
37 include IssueRelationsHelper
37 include IssueRelationsHelper
38 helper :watchers
38 helper :watchers
39 include WatchersHelper
39 include WatchersHelper
40 helper :attachments
40 helper :attachments
41 include AttachmentsHelper
41 include AttachmentsHelper
42 helper :queries
42 helper :queries
43 include QueriesHelper
43 helper :sort
44 helper :sort
44 include SortHelper
45 include SortHelper
45 include IssuesHelper
46 include IssuesHelper
46 helper :timelog
47 helper :timelog
47 include Redmine::Export::PDF
48 include Redmine::Export::PDF
48
49
49 verify :method => [:post, :delete],
50 verify :method => [:post, :delete],
50 :only => :destroy,
51 :only => :destroy,
51 :render => { :nothing => true, :status => :method_not_allowed }
52 :render => { :nothing => true, :status => :method_not_allowed }
52
53
53 def index
54 def index
54 retrieve_query
55 retrieve_query
55 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
56 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
56 sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.available_columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
57 sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.available_columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
57
58
58 if @query.valid?
59 if @query.valid?
59 limit = per_page_option
60 limit = per_page_option
60 respond_to do |format|
61 respond_to do |format|
61 format.html { }
62 format.html { }
62 format.xml { }
63 format.xml { }
63 format.atom { limit = Setting.feeds_limit.to_i }
64 format.atom { limit = Setting.feeds_limit.to_i }
64 format.csv { limit = Setting.issues_export_limit.to_i }
65 format.csv { limit = Setting.issues_export_limit.to_i }
65 format.pdf { limit = Setting.issues_export_limit.to_i }
66 format.pdf { limit = Setting.issues_export_limit.to_i }
66 end
67 end
67
68
68 @issue_count = @query.issue_count
69 @issue_count = @query.issue_count
69 @issue_pages = Paginator.new self, @issue_count, limit, params['page']
70 @issue_pages = Paginator.new self, @issue_count, limit, params['page']
70 @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
71 @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
71 :order => sort_clause,
72 :order => sort_clause,
72 :offset => @issue_pages.current.offset,
73 :offset => @issue_pages.current.offset,
73 :limit => limit)
74 :limit => limit)
74 @issue_count_by_group = @query.issue_count_by_group
75 @issue_count_by_group = @query.issue_count_by_group
75
76
76 respond_to do |format|
77 respond_to do |format|
77 format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
78 format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
78 format.xml { render :layout => false }
79 format.xml { render :layout => false }
79 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
80 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
80 format.csv { send_data(issues_to_csv(@issues, @project), :type => 'text/csv; header=present', :filename => 'export.csv') }
81 format.csv { send_data(issues_to_csv(@issues, @project), :type => 'text/csv; header=present', :filename => 'export.csv') }
81 format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'export.pdf') }
82 format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'export.pdf') }
82 end
83 end
83 else
84 else
84 # Send html if the query is not valid
85 # Send html if the query is not valid
85 render(:template => 'issues/index.rhtml', :layout => !request.xhr?)
86 render(:template => 'issues/index.rhtml', :layout => !request.xhr?)
86 end
87 end
87 rescue ActiveRecord::RecordNotFound
88 rescue ActiveRecord::RecordNotFound
88 render_404
89 render_404
89 end
90 end
90
91
91 def changes
92 def changes
92 retrieve_query
93 retrieve_query
93 sort_init 'id', 'desc'
94 sort_init 'id', 'desc'
94 sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.available_columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
95 sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.available_columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
95
96
96 if @query.valid?
97 if @query.valid?
97 @journals = @query.journals(:order => "#{Journal.table_name}.created_on DESC",
98 @journals = @query.journals(:order => "#{Journal.table_name}.created_on DESC",
98 :limit => 25)
99 :limit => 25)
99 end
100 end
100 @title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
101 @title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
101 render :layout => false, :content_type => 'application/atom+xml'
102 render :layout => false, :content_type => 'application/atom+xml'
102 rescue ActiveRecord::RecordNotFound
103 rescue ActiveRecord::RecordNotFound
103 render_404
104 render_404
104 end
105 end
105
106
106 def show
107 def show
107 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
108 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
108 @journals.each_with_index {|j,i| j.indice = i+1}
109 @journals.each_with_index {|j,i| j.indice = i+1}
109 @journals.reverse! if User.current.wants_comments_in_reverse_order?
110 @journals.reverse! if User.current.wants_comments_in_reverse_order?
110 @changesets = @issue.changesets
111 @changesets = @issue.changesets
111 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
112 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
112 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
113 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
113 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
114 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
114 @priorities = IssuePriority.all
115 @priorities = IssuePriority.all
115 @time_entry = TimeEntry.new
116 @time_entry = TimeEntry.new
116 respond_to do |format|
117 respond_to do |format|
117 format.html { render :template => 'issues/show.rhtml' }
118 format.html { render :template => 'issues/show.rhtml' }
118 format.xml { render :layout => false }
119 format.xml { render :layout => false }
119 format.atom { render :action => 'changes', :layout => false, :content_type => 'application/atom+xml' }
120 format.atom { render :action => 'changes', :layout => false, :content_type => 'application/atom+xml' }
120 format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
121 format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
121 end
122 end
122 end
123 end
123
124
124 # Add a new issue
125 # Add a new issue
125 # The new issue will be created from an existing one if copy_from parameter is given
126 # The new issue will be created from an existing one if copy_from parameter is given
126 def new
127 def new
127 @issue = Issue.new
128 @issue = Issue.new
128 @issue.copy_from(params[:copy_from]) if params[:copy_from]
129 @issue.copy_from(params[:copy_from]) if params[:copy_from]
129 @issue.project = @project
130 @issue.project = @project
130 # Tracker must be set before custom field values
131 # Tracker must be set before custom field values
131 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
132 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
132 if @issue.tracker.nil?
133 if @issue.tracker.nil?
133 render_error l(:error_no_tracker_in_project)
134 render_error l(:error_no_tracker_in_project)
134 return
135 return
135 end
136 end
136 if params[:issue].is_a?(Hash)
137 if params[:issue].is_a?(Hash)
137 @issue.safe_attributes = params[:issue]
138 @issue.safe_attributes = params[:issue]
138 @issue.watcher_user_ids = params[:issue]['watcher_user_ids'] if User.current.allowed_to?(:add_issue_watchers, @project)
139 @issue.watcher_user_ids = params[:issue]['watcher_user_ids'] if User.current.allowed_to?(:add_issue_watchers, @project)
139 end
140 end
140 @issue.author = User.current
141 @issue.author = User.current
141
142
142 default_status = IssueStatus.default
143 default_status = IssueStatus.default
143 unless default_status
144 unless default_status
144 render_error l(:error_no_default_issue_status)
145 render_error l(:error_no_default_issue_status)
145 return
146 return
146 end
147 end
147 @issue.status = default_status
148 @issue.status = default_status
148 @allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(User.current.roles_for_project(@project), @issue.tracker)).uniq
149 @allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(User.current.roles_for_project(@project), @issue.tracker)).uniq
149
150
150 if request.get? || request.xhr?
151 if request.get? || request.xhr?
151 @issue.start_date ||= Date.today
152 @issue.start_date ||= Date.today
152 else
153 else
153 requested_status = IssueStatus.find_by_id(params[:issue][:status_id])
154 requested_status = IssueStatus.find_by_id(params[:issue][:status_id])
154 # Check that the user is allowed to apply the requested status
155 # Check that the user is allowed to apply the requested status
155 @issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status
156 @issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status
156 call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
157 call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
157 if @issue.save
158 if @issue.save
158 attach_files(@issue, params[:attachments])
159 attach_files(@issue, params[:attachments])
159 flash[:notice] = l(:notice_successful_create)
160 flash[:notice] = l(:notice_successful_create)
160 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
161 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
161 respond_to do |format|
162 respond_to do |format|
162 format.html {
163 format.html {
163 redirect_to(params[:continue] ? { :action => 'new', :tracker_id => @issue.tracker } :
164 redirect_to(params[:continue] ? { :action => 'new', :tracker_id => @issue.tracker } :
164 { :action => 'show', :id => @issue })
165 { :action => 'show', :id => @issue })
165 }
166 }
166 format.xml { render :action => 'show', :status => :created, :location => url_for(:controller => 'issues', :action => 'show', :id => @issue) }
167 format.xml { render :action => 'show', :status => :created, :location => url_for(:controller => 'issues', :action => 'show', :id => @issue) }
167 end
168 end
168 return
169 return
169 else
170 else
170 respond_to do |format|
171 respond_to do |format|
171 format.html { }
172 format.html { }
172 format.xml { render(:xml => @issue.errors, :status => :unprocessable_entity); return }
173 format.xml { render(:xml => @issue.errors, :status => :unprocessable_entity); return }
173 end
174 end
174 end
175 end
175 end
176 end
176 @priorities = IssuePriority.all
177 @priorities = IssuePriority.all
177 render :layout => !request.xhr?
178 render :layout => !request.xhr?
178 end
179 end
179
180
180 # Attributes that can be updated on workflow transition (without :edit permission)
181 # Attributes that can be updated on workflow transition (without :edit permission)
181 # TODO: make it configurable (at least per role)
182 # TODO: make it configurable (at least per role)
182 UPDATABLE_ATTRS_ON_TRANSITION = %w(status_id assigned_to_id fixed_version_id done_ratio) unless const_defined?(:UPDATABLE_ATTRS_ON_TRANSITION)
183 UPDATABLE_ATTRS_ON_TRANSITION = %w(status_id assigned_to_id fixed_version_id done_ratio) unless const_defined?(:UPDATABLE_ATTRS_ON_TRANSITION)
183
184
184 def edit
185 def edit
185 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
186 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
186 @priorities = IssuePriority.all
187 @priorities = IssuePriority.all
187 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
188 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
188 @time_entry = TimeEntry.new
189 @time_entry = TimeEntry.new
189
190
190 @notes = params[:notes]
191 @notes = params[:notes]
191 journal = @issue.init_journal(User.current, @notes)
192 journal = @issue.init_journal(User.current, @notes)
192 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
193 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
193 if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue]
194 if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue]
194 attrs = params[:issue].dup
195 attrs = params[:issue].dup
195 attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed
196 attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed
196 attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s}
197 attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s}
197 @issue.safe_attributes = attrs
198 @issue.safe_attributes = attrs
198 end
199 end
199
200
200 if request.get?
201 if request.get?
201 # nop
202 # nop
202 else
203 else
203 @time_entry = TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
204 @time_entry = TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
204 @time_entry.attributes = params[:time_entry]
205 @time_entry.attributes = params[:time_entry]
205 if (@time_entry.hours.nil? || @time_entry.valid?) && @issue.valid?
206 if (@time_entry.hours.nil? || @time_entry.valid?) && @issue.valid?
206 attachments = attach_files(@issue, params[:attachments])
207 attachments = attach_files(@issue, params[:attachments])
207 attachments.each {|a| journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
208 attachments.each {|a| journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
208 call_hook(:controller_issues_edit_before_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
209 call_hook(:controller_issues_edit_before_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
209 if @issue.save
210 if @issue.save
210 # Log spend time
211 # Log spend time
211 if User.current.allowed_to?(:log_time, @project)
212 if User.current.allowed_to?(:log_time, @project)
212 @time_entry.save
213 @time_entry.save
213 end
214 end
214 if !journal.new_record?
215 if !journal.new_record?
215 # Only send notification if something was actually changed
216 # Only send notification if something was actually changed
216 flash[:notice] = l(:notice_successful_update)
217 flash[:notice] = l(:notice_successful_update)
217 end
218 end
218 call_hook(:controller_issues_edit_after_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
219 call_hook(:controller_issues_edit_after_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
219 respond_to do |format|
220 respond_to do |format|
220 format.html { redirect_back_or_default({:action => 'show', :id => @issue}) }
221 format.html { redirect_back_or_default({:action => 'show', :id => @issue}) }
221 format.xml { head :ok }
222 format.xml { head :ok }
222 end
223 end
223 return
224 return
224 end
225 end
225 end
226 end
226 # failure
227 # failure
227 respond_to do |format|
228 respond_to do |format|
228 format.html { }
229 format.html { }
229 format.xml { render :xml => @issue.errors, :status => :unprocessable_entity }
230 format.xml { render :xml => @issue.errors, :status => :unprocessable_entity }
230 end
231 end
231 end
232 end
232 rescue ActiveRecord::StaleObjectError
233 rescue ActiveRecord::StaleObjectError
233 # Optimistic locking exception
234 # Optimistic locking exception
234 flash.now[:error] = l(:notice_locking_conflict)
235 flash.now[:error] = l(:notice_locking_conflict)
235 # Remove the previously added attachments if issue was not updated
236 # Remove the previously added attachments if issue was not updated
236 attachments.each(&:destroy)
237 attachments.each(&:destroy)
237 end
238 end
238
239
239 def reply
240 def reply
240 journal = Journal.find(params[:journal_id]) if params[:journal_id]
241 journal = Journal.find(params[:journal_id]) if params[:journal_id]
241 if journal
242 if journal
242 user = journal.user
243 user = journal.user
243 text = journal.notes
244 text = journal.notes
244 else
245 else
245 user = @issue.author
246 user = @issue.author
246 text = @issue.description
247 text = @issue.description
247 end
248 end
248 content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
249 content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
249 content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
250 content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
250 render(:update) { |page|
251 render(:update) { |page|
251 page.<< "$('notes').value = \"#{content}\";"
252 page.<< "$('notes').value = \"#{content}\";"
252 page.show 'update'
253 page.show 'update'
253 page << "Form.Element.focus('notes');"
254 page << "Form.Element.focus('notes');"
254 page << "Element.scrollTo('update');"
255 page << "Element.scrollTo('update');"
255 page << "$('notes').scrollTop = $('notes').scrollHeight - $('notes').clientHeight;"
256 page << "$('notes').scrollTop = $('notes').scrollHeight - $('notes').clientHeight;"
256 }
257 }
257 end
258 end
258
259
259 # Bulk edit a set of issues
260 # Bulk edit a set of issues
260 def bulk_edit
261 def bulk_edit
261 if request.post?
262 if request.post?
262 tracker = params[:tracker_id].blank? ? nil : @project.trackers.find_by_id(params[:tracker_id])
263 tracker = params[:tracker_id].blank? ? nil : @project.trackers.find_by_id(params[:tracker_id])
263 status = params[:status_id].blank? ? nil : IssueStatus.find_by_id(params[:status_id])
264 status = params[:status_id].blank? ? nil : IssueStatus.find_by_id(params[:status_id])
264 priority = params[:priority_id].blank? ? nil : IssuePriority.find_by_id(params[:priority_id])
265 priority = params[:priority_id].blank? ? nil : IssuePriority.find_by_id(params[:priority_id])
265 assigned_to = (params[:assigned_to_id].blank? || params[:assigned_to_id] == 'none') ? nil : User.find_by_id(params[:assigned_to_id])
266 assigned_to = (params[:assigned_to_id].blank? || params[:assigned_to_id] == 'none') ? nil : User.find_by_id(params[:assigned_to_id])
266 category = (params[:category_id].blank? || params[:category_id] == 'none') ? nil : @project.issue_categories.find_by_id(params[:category_id])
267 category = (params[:category_id].blank? || params[:category_id] == 'none') ? nil : @project.issue_categories.find_by_id(params[:category_id])
267 fixed_version = (params[:fixed_version_id].blank? || params[:fixed_version_id] == 'none') ? nil : @project.shared_versions.find_by_id(params[:fixed_version_id])
268 fixed_version = (params[:fixed_version_id].blank? || params[:fixed_version_id] == 'none') ? nil : @project.shared_versions.find_by_id(params[:fixed_version_id])
268 custom_field_values = params[:custom_field_values] ? params[:custom_field_values].reject {|k,v| v.blank?} : nil
269 custom_field_values = params[:custom_field_values] ? params[:custom_field_values].reject {|k,v| v.blank?} : nil
269
270
270 unsaved_issue_ids = []
271 unsaved_issue_ids = []
271 @issues.each do |issue|
272 @issues.each do |issue|
272 journal = issue.init_journal(User.current, params[:notes])
273 journal = issue.init_journal(User.current, params[:notes])
273 issue.tracker = tracker if tracker
274 issue.tracker = tracker if tracker
274 issue.priority = priority if priority
275 issue.priority = priority if priority
275 issue.assigned_to = assigned_to if assigned_to || params[:assigned_to_id] == 'none'
276 issue.assigned_to = assigned_to if assigned_to || params[:assigned_to_id] == 'none'
276 issue.category = category if category || params[:category_id] == 'none'
277 issue.category = category if category || params[:category_id] == 'none'
277 issue.fixed_version = fixed_version if fixed_version || params[:fixed_version_id] == 'none'
278 issue.fixed_version = fixed_version if fixed_version || params[:fixed_version_id] == 'none'
278 issue.start_date = params[:start_date] unless params[:start_date].blank?
279 issue.start_date = params[:start_date] unless params[:start_date].blank?
279 issue.due_date = params[:due_date] unless params[:due_date].blank?
280 issue.due_date = params[:due_date] unless params[:due_date].blank?
280 issue.done_ratio = params[:done_ratio] unless params[:done_ratio].blank?
281 issue.done_ratio = params[:done_ratio] unless params[:done_ratio].blank?
281 issue.custom_field_values = custom_field_values if custom_field_values && !custom_field_values.empty?
282 issue.custom_field_values = custom_field_values if custom_field_values && !custom_field_values.empty?
282 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
283 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
283 # Don't save any change to the issue if the user is not authorized to apply the requested status
284 # Don't save any change to the issue if the user is not authorized to apply the requested status
284 unless (status.nil? || (issue.new_statuses_allowed_to(User.current).include?(status) && issue.status = status)) && issue.save
285 unless (status.nil? || (issue.new_statuses_allowed_to(User.current).include?(status) && issue.status = status)) && issue.save
285 # Keep unsaved issue ids to display them in flash error
286 # Keep unsaved issue ids to display them in flash error
286 unsaved_issue_ids << issue.id
287 unsaved_issue_ids << issue.id
287 end
288 end
288 end
289 end
289 if unsaved_issue_ids.empty?
290 if unsaved_issue_ids.empty?
290 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
291 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
291 else
292 else
292 flash[:error] = l(:notice_failed_to_save_issues, :count => unsaved_issue_ids.size,
293 flash[:error] = l(:notice_failed_to_save_issues, :count => unsaved_issue_ids.size,
293 :total => @issues.size,
294 :total => @issues.size,
294 :ids => '#' + unsaved_issue_ids.join(', #'))
295 :ids => '#' + unsaved_issue_ids.join(', #'))
295 end
296 end
296 redirect_back_or_default({:controller => 'issues', :action => 'index', :project_id => @project})
297 redirect_back_or_default({:controller => 'issues', :action => 'index', :project_id => @project})
297 return
298 return
298 end
299 end
299 @available_statuses = Workflow.available_statuses(@project)
300 @available_statuses = Workflow.available_statuses(@project)
300 @custom_fields = @project.all_issue_custom_fields
301 @custom_fields = @project.all_issue_custom_fields
301 end
302 end
302
303
303 def move
304 def move
304 @copy = params[:copy_options] && params[:copy_options][:copy]
305 @copy = params[:copy_options] && params[:copy_options][:copy]
305 @allowed_projects = []
306 @allowed_projects = []
306 # find projects to which the user is allowed to move the issue
307 # find projects to which the user is allowed to move the issue
307 if User.current.admin?
308 if User.current.admin?
308 # admin is allowed to move issues to any active (visible) project
309 # admin is allowed to move issues to any active (visible) project
309 @allowed_projects = Project.find(:all, :conditions => Project.visible_by(User.current))
310 @allowed_projects = Project.find(:all, :conditions => Project.visible_by(User.current))
310 else
311 else
311 User.current.memberships.each {|m| @allowed_projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}}
312 User.current.memberships.each {|m| @allowed_projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}}
312 end
313 end
313 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id]
314 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id]
314 @target_project ||= @project
315 @target_project ||= @project
315 @trackers = @target_project.trackers
316 @trackers = @target_project.trackers
316 @available_statuses = Workflow.available_statuses(@project)
317 @available_statuses = Workflow.available_statuses(@project)
317 if request.post?
318 if request.post?
318 new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id])
319 new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id])
319 unsaved_issue_ids = []
320 unsaved_issue_ids = []
320 moved_issues = []
321 moved_issues = []
321 @issues.each do |issue|
322 @issues.each do |issue|
322 changed_attributes = {}
323 changed_attributes = {}
323 [:assigned_to_id, :status_id, :start_date, :due_date].each do |valid_attribute|
324 [:assigned_to_id, :status_id, :start_date, :due_date].each do |valid_attribute|
324 unless params[valid_attribute].blank?
325 unless params[valid_attribute].blank?
325 changed_attributes[valid_attribute] = (params[valid_attribute] == 'none' ? nil : params[valid_attribute])
326 changed_attributes[valid_attribute] = (params[valid_attribute] == 'none' ? nil : params[valid_attribute])
326 end
327 end
327 end
328 end
328 issue.init_journal(User.current)
329 issue.init_journal(User.current)
329 if r = issue.move_to(@target_project, new_tracker, {:copy => @copy, :attributes => changed_attributes})
330 if r = issue.move_to(@target_project, new_tracker, {:copy => @copy, :attributes => changed_attributes})
330 moved_issues << r
331 moved_issues << r
331 else
332 else
332 unsaved_issue_ids << issue.id
333 unsaved_issue_ids << issue.id
333 end
334 end
334 end
335 end
335 if unsaved_issue_ids.empty?
336 if unsaved_issue_ids.empty?
336 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
337 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
337 else
338 else
338 flash[:error] = l(:notice_failed_to_save_issues, :count => unsaved_issue_ids.size,
339 flash[:error] = l(:notice_failed_to_save_issues, :count => unsaved_issue_ids.size,
339 :total => @issues.size,
340 :total => @issues.size,
340 :ids => '#' + unsaved_issue_ids.join(', #'))
341 :ids => '#' + unsaved_issue_ids.join(', #'))
341 end
342 end
342 if params[:follow]
343 if params[:follow]
343 if @issues.size == 1 && moved_issues.size == 1
344 if @issues.size == 1 && moved_issues.size == 1
344 redirect_to :controller => 'issues', :action => 'show', :id => moved_issues.first
345 redirect_to :controller => 'issues', :action => 'show', :id => moved_issues.first
345 else
346 else
346 redirect_to :controller => 'issues', :action => 'index', :project_id => (@target_project || @project)
347 redirect_to :controller => 'issues', :action => 'index', :project_id => (@target_project || @project)
347 end
348 end
348 else
349 else
349 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
350 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
350 end
351 end
351 return
352 return
352 end
353 end
353 render :layout => false if request.xhr?
354 render :layout => false if request.xhr?
354 end
355 end
355
356
356 def destroy
357 def destroy
357 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
358 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
358 if @hours > 0
359 if @hours > 0
359 case params[:todo]
360 case params[:todo]
360 when 'destroy'
361 when 'destroy'
361 # nothing to do
362 # nothing to do
362 when 'nullify'
363 when 'nullify'
363 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
364 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
364 when 'reassign'
365 when 'reassign'
365 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
366 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
366 if reassign_to.nil?
367 if reassign_to.nil?
367 flash.now[:error] = l(:error_issue_not_found_in_project)
368 flash.now[:error] = l(:error_issue_not_found_in_project)
368 return
369 return
369 else
370 else
370 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
371 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
371 end
372 end
372 else
373 else
373 unless params[:format] == 'xml'
374 unless params[:format] == 'xml'
374 # display the destroy form if it's a user request
375 # display the destroy form if it's a user request
375 return
376 return
376 end
377 end
377 end
378 end
378 end
379 end
379 @issues.each(&:destroy)
380 @issues.each(&:destroy)
380 respond_to do |format|
381 respond_to do |format|
381 format.html { redirect_to :action => 'index', :project_id => @project }
382 format.html { redirect_to :action => 'index', :project_id => @project }
382 format.xml { head :ok }
383 format.xml { head :ok }
383 end
384 end
384 end
385 end
385
386
386 def gantt
387 def gantt
387 @gantt = Redmine::Helpers::Gantt.new(params)
388 @gantt = Redmine::Helpers::Gantt.new(params)
388 retrieve_query
389 retrieve_query
389 if @query.valid?
390 if @query.valid?
390 events = []
391 events = []
391 # Issues that have start and due dates
392 # Issues that have start and due dates
392 events += @query.issues(:include => [:tracker, :assigned_to, :priority],
393 events += @query.issues(:include => [:tracker, :assigned_to, :priority],
393 :order => "start_date, due_date",
394 :order => "start_date, due_date",
394 :conditions => ["(((start_date>=? and start_date<=?) or (due_date>=? and due_date<=?) or (start_date<? and due_date>?)) and start_date is not null and due_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to]
395 :conditions => ["(((start_date>=? and start_date<=?) or (due_date>=? and due_date<=?) or (start_date<? and due_date>?)) and start_date is not null and due_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to]
395 )
396 )
396 # Issues that don't have a due date but that are assigned to a version with a date
397 # Issues that don't have a due date but that are assigned to a version with a date
397 events += @query.issues(:include => [:tracker, :assigned_to, :priority, :fixed_version],
398 events += @query.issues(:include => [:tracker, :assigned_to, :priority, :fixed_version],
398 :order => "start_date, effective_date",
399 :order => "start_date, effective_date",
399 :conditions => ["(((start_date>=? and start_date<=?) or (effective_date>=? and effective_date<=?) or (start_date<? and effective_date>?)) and start_date is not null and due_date is null and effective_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to]
400 :conditions => ["(((start_date>=? and start_date<=?) or (effective_date>=? and effective_date<=?) or (start_date<? and effective_date>?)) and start_date is not null and due_date is null and effective_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to]
400 )
401 )
401 # Versions
402 # Versions
402 events += @query.versions(:conditions => ["effective_date BETWEEN ? AND ?", @gantt.date_from, @gantt.date_to])
403 events += @query.versions(:conditions => ["effective_date BETWEEN ? AND ?", @gantt.date_from, @gantt.date_to])
403
404
404 @gantt.events = events
405 @gantt.events = events
405 end
406 end
406
407
407 basename = (@project ? "#{@project.identifier}-" : '') + 'gantt'
408 basename = (@project ? "#{@project.identifier}-" : '') + 'gantt'
408
409
409 respond_to do |format|
410 respond_to do |format|
410 format.html { render :template => "issues/gantt.rhtml", :layout => !request.xhr? }
411 format.html { render :template => "issues/gantt.rhtml", :layout => !request.xhr? }
411 format.png { send_data(@gantt.to_image, :disposition => 'inline', :type => 'image/png', :filename => "#{basename}.png") } if @gantt.respond_to?('to_image')
412 format.png { send_data(@gantt.to_image, :disposition => 'inline', :type => 'image/png', :filename => "#{basename}.png") } if @gantt.respond_to?('to_image')
412 format.pdf { send_data(gantt_to_pdf(@gantt, @project), :type => 'application/pdf', :filename => "#{basename}.pdf") }
413 format.pdf { send_data(gantt_to_pdf(@gantt, @project), :type => 'application/pdf', :filename => "#{basename}.pdf") }
413 end
414 end
414 end
415 end
415
416
416 def calendar
417 def calendar
417 if params[:year] and params[:year].to_i > 1900
418 if params[:year] and params[:year].to_i > 1900
418 @year = params[:year].to_i
419 @year = params[:year].to_i
419 if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
420 if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
420 @month = params[:month].to_i
421 @month = params[:month].to_i
421 end
422 end
422 end
423 end
423 @year ||= Date.today.year
424 @year ||= Date.today.year
424 @month ||= Date.today.month
425 @month ||= Date.today.month
425
426
426 @calendar = Redmine::Helpers::Calendar.new(Date.civil(@year, @month, 1), current_language, :month)
427 @calendar = Redmine::Helpers::Calendar.new(Date.civil(@year, @month, 1), current_language, :month)
427 retrieve_query
428 retrieve_query
428 if @query.valid?
429 if @query.valid?
429 events = []
430 events = []
430 events += @query.issues(:include => [:tracker, :assigned_to, :priority],
431 events += @query.issues(:include => [:tracker, :assigned_to, :priority],
431 :conditions => ["((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?))", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt]
432 :conditions => ["((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?))", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt]
432 )
433 )
433 events += @query.versions(:conditions => ["effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt])
434 events += @query.versions(:conditions => ["effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt])
434
435
435 @calendar.events = events
436 @calendar.events = events
436 end
437 end
437
438
438 render :layout => false if request.xhr?
439 render :layout => false if request.xhr?
439 end
440 end
440
441
441 def context_menu
442 def context_menu
442 @issues = Issue.find_all_by_id(params[:ids], :include => :project)
443 @issues = Issue.find_all_by_id(params[:ids], :include => :project)
443 if (@issues.size == 1)
444 if (@issues.size == 1)
444 @issue = @issues.first
445 @issue = @issues.first
445 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
446 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
446 end
447 end
447 projects = @issues.collect(&:project).compact.uniq
448 projects = @issues.collect(&:project).compact.uniq
448 @project = projects.first if projects.size == 1
449 @project = projects.first if projects.size == 1
449
450
450 @can = {:edit => (@project && User.current.allowed_to?(:edit_issues, @project)),
451 @can = {:edit => (@project && User.current.allowed_to?(:edit_issues, @project)),
451 :log_time => (@project && User.current.allowed_to?(:log_time, @project)),
452 :log_time => (@project && User.current.allowed_to?(:log_time, @project)),
452 :update => (@project && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && @allowed_statuses && !@allowed_statuses.empty?))),
453 :update => (@project && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && @allowed_statuses && !@allowed_statuses.empty?))),
453 :move => (@project && User.current.allowed_to?(:move_issues, @project)),
454 :move => (@project && User.current.allowed_to?(:move_issues, @project)),
454 :copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
455 :copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
455 :delete => (@project && User.current.allowed_to?(:delete_issues, @project))
456 :delete => (@project && User.current.allowed_to?(:delete_issues, @project))
456 }
457 }
457 if @project
458 if @project
458 @assignables = @project.assignable_users
459 @assignables = @project.assignable_users
459 @assignables << @issue.assigned_to if @issue && @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
460 @assignables << @issue.assigned_to if @issue && @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
460 @trackers = @project.trackers
461 @trackers = @project.trackers
461 end
462 end
462
463
463 @priorities = IssuePriority.all.reverse
464 @priorities = IssuePriority.all.reverse
464 @statuses = IssueStatus.find(:all, :order => 'position')
465 @statuses = IssueStatus.find(:all, :order => 'position')
465 @back = params[:back_url] || request.env['HTTP_REFERER']
466 @back = params[:back_url] || request.env['HTTP_REFERER']
466
467
467 render :layout => false
468 render :layout => false
468 end
469 end
469
470
470 def update_form
471 def update_form
471 if params[:id].blank?
472 if params[:id].blank?
472 @issue = Issue.new
473 @issue = Issue.new
473 @issue.project = @project
474 @issue.project = @project
474 else
475 else
475 @issue = @project.issues.visible.find(params[:id])
476 @issue = @project.issues.visible.find(params[:id])
476 end
477 end
477 @issue.attributes = params[:issue]
478 @issue.attributes = params[:issue]
478 @allowed_statuses = ([@issue.status] + @issue.status.find_new_statuses_allowed_to(User.current.roles_for_project(@project), @issue.tracker)).uniq
479 @allowed_statuses = ([@issue.status] + @issue.status.find_new_statuses_allowed_to(User.current.roles_for_project(@project), @issue.tracker)).uniq
479 @priorities = IssuePriority.all
480 @priorities = IssuePriority.all
480
481
481 render :partial => 'attributes'
482 render :partial => 'attributes'
482 end
483 end
483
484
484 def preview
485 def preview
485 @issue = @project.issues.find_by_id(params[:id]) unless params[:id].blank?
486 @issue = @project.issues.find_by_id(params[:id]) unless params[:id].blank?
486 @attachements = @issue.attachments if @issue
487 @attachements = @issue.attachments if @issue
487 @text = params[:notes] || (params[:issue] ? params[:issue][:description] : nil)
488 @text = params[:notes] || (params[:issue] ? params[:issue][:description] : nil)
488 render :partial => 'common/preview'
489 render :partial => 'common/preview'
489 end
490 end
490
491
491 private
492 private
492 def find_issue
493 def find_issue
493 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
494 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
494 @project = @issue.project
495 @project = @issue.project
495 rescue ActiveRecord::RecordNotFound
496 rescue ActiveRecord::RecordNotFound
496 render_404
497 render_404
497 end
498 end
498
499
499 # Filter for bulk operations
500 # Filter for bulk operations
500 def find_issues
501 def find_issues
501 @issues = Issue.find_all_by_id(params[:id] || params[:ids])
502 @issues = Issue.find_all_by_id(params[:id] || params[:ids])
502 raise ActiveRecord::RecordNotFound if @issues.empty?
503 raise ActiveRecord::RecordNotFound if @issues.empty?
503 projects = @issues.collect(&:project).compact.uniq
504 projects = @issues.collect(&:project).compact.uniq
504 if projects.size == 1
505 if projects.size == 1
505 @project = projects.first
506 @project = projects.first
506 else
507 else
507 # TODO: let users bulk edit/move/destroy issues from different projects
508 # TODO: let users bulk edit/move/destroy issues from different projects
508 render_error 'Can not bulk edit/move/destroy issues from different projects'
509 render_error 'Can not bulk edit/move/destroy issues from different projects'
509 return false
510 return false
510 end
511 end
511 rescue ActiveRecord::RecordNotFound
512 rescue ActiveRecord::RecordNotFound
512 render_404
513 render_404
513 end
514 end
514
515
515 def find_project
516 def find_project
516 project_id = (params[:issue] && params[:issue][:project_id]) || params[:project_id]
517 project_id = (params[:issue] && params[:issue][:project_id]) || params[:project_id]
517 @project = Project.find(project_id)
518 @project = Project.find(project_id)
518 rescue ActiveRecord::RecordNotFound
519 rescue ActiveRecord::RecordNotFound
519 render_404
520 render_404
520 end
521 end
521
522
522 def find_optional_project
523 def find_optional_project
523 @project = Project.find(params[:project_id]) unless params[:project_id].blank?
524 @project = Project.find(params[:project_id]) unless params[:project_id].blank?
524 allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
525 allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
525 allowed ? true : deny_access
526 allowed ? true : deny_access
526 rescue ActiveRecord::RecordNotFound
527 rescue ActiveRecord::RecordNotFound
527 render_404
528 render_404
528 end
529 end
529
530
530 # Retrieve query from session or build a new query
531 # Retrieve query from session or build a new query
531 def retrieve_query
532 def retrieve_query
532 if !params[:query_id].blank?
533 if !params[:query_id].blank?
533 cond = "project_id IS NULL"
534 cond = "project_id IS NULL"
534 cond << " OR project_id = #{@project.id}" if @project
535 cond << " OR project_id = #{@project.id}" if @project
535 @query = Query.find(params[:query_id], :conditions => cond)
536 @query = Query.find(params[:query_id], :conditions => cond)
536 @query.project = @project
537 @query.project = @project
537 session[:query] = {:id => @query.id, :project_id => @query.project_id}
538 session[:query] = {:id => @query.id, :project_id => @query.project_id}
538 sort_clear
539 sort_clear
539 else
540 else
540 if params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
541 if params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
541 # Give it a name, required to be valid
542 # Give it a name, required to be valid
542 @query = Query.new(:name => "_")
543 @query = Query.new(:name => "_")
543 @query.project = @project
544 @query.project = @project
544 if params[:fields] and params[:fields].is_a? Array
545 if params[:fields] and params[:fields].is_a? Array
545 params[:fields].each do |field|
546 params[:fields].each do |field|
546 @query.add_filter(field, params[:operators][field], params[:values][field])
547 @query.add_filter(field, params[:operators][field], params[:values][field])
547 end
548 end
548 else
549 else
549 @query.available_filters.keys.each do |field|
550 @query.available_filters.keys.each do |field|
550 @query.add_short_filter(field, params[field]) if params[field]
551 @query.add_short_filter(field, params[field]) if params[field]
551 end
552 end
552 end
553 end
553 @query.group_by = params[:group_by]
554 @query.group_by = params[:group_by]
554 @query.column_names = params[:query] && params[:query][:column_names]
555 @query.column_names = params[:query] && params[:query][:column_names]
555 session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names}
556 session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names}
556 else
557 else
557 @query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
558 @query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
558 @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
559 @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
559 @query.project = @project
560 @query.project = @project
560 end
561 end
561 end
562 end
562 end
563 end
563
564
564 # Rescues an invalid query statement. Just in case...
565 # Rescues an invalid query statement. Just in case...
565 def query_statement_invalid(exception)
566 def query_statement_invalid(exception)
566 logger.error "Query::StatementInvalid: #{exception.message}" if logger
567 logger.error "Query::StatementInvalid: #{exception.message}" if logger
567 session.delete(:query)
568 session.delete(:query)
568 sort_clear
569 sort_clear
569 render_error "An error occurred while executing the query and has been logged. Please report this error to your Redmine administrator."
570 render_error "An error occurred while executing the query and has been logged. Please report this error to your Redmine administrator."
570 end
571 end
571 end
572 end
@@ -1,487 +1,489
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2009 Jean-Philippe Lang
4 # Copyright (C) 2006-2009 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 require 'iconv'
20 require 'iconv'
21 require 'rfpdf/fpdf'
21 require 'rfpdf/fpdf'
22 require 'rfpdf/chinese'
22 require 'rfpdf/chinese'
23
23
24 module Redmine
24 module Redmine
25 module Export
25 module Export
26 module PDF
26 module PDF
27 include ActionView::Helpers::TextHelper
27 include ActionView::Helpers::TextHelper
28 include ActionView::Helpers::NumberHelper
28 include ActionView::Helpers::NumberHelper
29
29
30 class IFPDF < FPDF
30 class IFPDF < FPDF
31 include Redmine::I18n
31 include Redmine::I18n
32 attr_accessor :footer_date
32 attr_accessor :footer_date
33
33
34 def initialize(lang)
34 def initialize(lang)
35 super()
35 super()
36 set_language_if_valid lang
36 set_language_if_valid lang
37 case current_language.to_s.downcase
37 case current_language.to_s.downcase
38 when 'ja'
38 when 'ja'
39 extend(PDF_Japanese)
39 extend(PDF_Japanese)
40 AddSJISFont()
40 AddSJISFont()
41 @font_for_content = 'SJIS'
41 @font_for_content = 'SJIS'
42 @font_for_footer = 'SJIS'
42 @font_for_footer = 'SJIS'
43 when 'zh'
43 when 'zh'
44 extend(PDF_Chinese)
44 extend(PDF_Chinese)
45 AddGBFont()
45 AddGBFont()
46 @font_for_content = 'GB'
46 @font_for_content = 'GB'
47 @font_for_footer = 'GB'
47 @font_for_footer = 'GB'
48 when 'zh-tw'
48 when 'zh-tw'
49 extend(PDF_Chinese)
49 extend(PDF_Chinese)
50 AddBig5Font()
50 AddBig5Font()
51 @font_for_content = 'Big5'
51 @font_for_content = 'Big5'
52 @font_for_footer = 'Big5'
52 @font_for_footer = 'Big5'
53 else
53 else
54 @font_for_content = 'Arial'
54 @font_for_content = 'Arial'
55 @font_for_footer = 'Helvetica'
55 @font_for_footer = 'Helvetica'
56 end
56 end
57 SetCreator(Redmine::Info.app_name)
57 SetCreator(Redmine::Info.app_name)
58 SetFont(@font_for_content)
58 SetFont(@font_for_content)
59 end
59 end
60
60
61 def SetFontStyle(style, size)
61 def SetFontStyle(style, size)
62 SetFont(@font_for_content, style, size)
62 SetFont(@font_for_content, style, size)
63 end
63 end
64
64
65 def SetTitle(txt)
65 def SetTitle(txt)
66 txt = begin
66 txt = begin
67 utf16txt = Iconv.conv('UTF-16BE', 'UTF-8', txt)
67 utf16txt = Iconv.conv('UTF-16BE', 'UTF-8', txt)
68 hextxt = "<FEFF" # FEFF is BOM
68 hextxt = "<FEFF" # FEFF is BOM
69 hextxt << utf16txt.unpack("C*").map {|x| sprintf("%02X",x) }.join
69 hextxt << utf16txt.unpack("C*").map {|x| sprintf("%02X",x) }.join
70 hextxt << ">"
70 hextxt << ">"
71 rescue
71 rescue
72 txt
72 txt
73 end || ''
73 end || ''
74 super(txt)
74 super(txt)
75 end
75 end
76
76
77 def textstring(s)
77 def textstring(s)
78 # Format a text string
78 # Format a text string
79 if s =~ /^</ # This means the string is hex-dumped.
79 if s =~ /^</ # This means the string is hex-dumped.
80 return s
80 return s
81 else
81 else
82 return '('+escape(s)+')'
82 return '('+escape(s)+')'
83 end
83 end
84 end
84 end
85
85
86 def Cell(w,h=0,txt='',border=0,ln=0,align='',fill=0,link='')
86 def Cell(w,h=0,txt='',border=0,ln=0,align='',fill=0,link='')
87 @ic ||= Iconv.new(l(:general_pdf_encoding), 'UTF-8')
87 @ic ||= Iconv.new(l(:general_pdf_encoding), 'UTF-8')
88 # these quotation marks are not correctly rendered in the pdf
88 # these quotation marks are not correctly rendered in the pdf
89 txt = txt.gsub(/[Ò€œÒ€�]/, '"') if txt
89 txt = txt.gsub(/[Ò€œÒ€�]/, '"') if txt
90 txt = begin
90 txt = begin
91 # 0x5c char handling
91 # 0x5c char handling
92 txtar = txt.split('\\')
92 txtar = txt.split('\\')
93 txtar << '' if txt[-1] == ?\\
93 txtar << '' if txt[-1] == ?\\
94 txtar.collect {|x| @ic.iconv(x)}.join('\\').gsub(/\\/, "\\\\\\\\")
94 txtar.collect {|x| @ic.iconv(x)}.join('\\').gsub(/\\/, "\\\\\\\\")
95 rescue
95 rescue
96 txt
96 txt
97 end || ''
97 end || ''
98 super w,h,txt,border,ln,align,fill,link
98 super w,h,txt,border,ln,align,fill,link
99 end
99 end
100
100
101 def Footer
101 def Footer
102 SetFont(@font_for_footer, 'I', 8)
102 SetFont(@font_for_footer, 'I', 8)
103 SetY(-15)
103 SetY(-15)
104 SetX(15)
104 SetX(15)
105 Cell(0, 5, @footer_date, 0, 0, 'L')
105 Cell(0, 5, @footer_date, 0, 0, 'L')
106 SetY(-15)
106 SetY(-15)
107 SetX(-30)
107 SetX(-30)
108 Cell(0, 5, PageNo().to_s + '/{nb}', 0, 0, 'C')
108 Cell(0, 5, PageNo().to_s + '/{nb}', 0, 0, 'C')
109 end
109 end
110 end
110 end
111
111
112 # Returns a PDF string of a list of issues
112 # Returns a PDF string of a list of issues
113 def issues_to_pdf(issues, project, query)
113 def issues_to_pdf(issues, project, query)
114 pdf = IFPDF.new(current_language)
114 pdf = IFPDF.new(current_language)
115 title = query.new_record? ? l(:label_issue_plural) : query.name
115 title = query.new_record? ? l(:label_issue_plural) : query.name
116 title = "#{project} - #{title}" if project
116 title = "#{project} - #{title}" if project
117 pdf.SetTitle(title)
117 pdf.SetTitle(title)
118 pdf.AliasNbPages
118 pdf.AliasNbPages
119 pdf.footer_date = format_date(Date.today)
119 pdf.footer_date = format_date(Date.today)
120 pdf.AddPage("L")
120 pdf.AddPage("L")
121
121
122 row_height = 6
122 row_height = 6
123 col_width = []
123 col_width = []
124 unless query.columns.empty?
124 unless query.columns.empty?
125 col_width = query.columns.collect {|column| column.name == :subject ? 4.0 : 1.0 }
125 col_width = query.columns.collect {|column| column.name == :subject ? 4.0 : 1.0 }
126 ratio = 262.0 / col_width.inject(0) {|s,w| s += w}
126 ratio = 262.0 / col_width.inject(0) {|s,w| s += w}
127 col_width = col_width.collect {|w| w * ratio}
127 col_width = col_width.collect {|w| w * ratio}
128 end
128 end
129
129
130 # title
130 # title
131 pdf.SetFontStyle('B',11)
131 pdf.SetFontStyle('B',11)
132 pdf.Cell(190,10, title)
132 pdf.Cell(190,10, title)
133 pdf.Ln
133 pdf.Ln
134
134
135 # headers
135 # headers
136 pdf.SetFontStyle('B',8)
136 pdf.SetFontStyle('B',8)
137 pdf.SetFillColor(230, 230, 230)
137 pdf.SetFillColor(230, 230, 230)
138 pdf.Cell(15, row_height, "#", 1, 0, 'L', 1)
138 pdf.Cell(15, row_height, "#", 1, 0, 'L', 1)
139 query.columns.each_with_index do |column, i|
139 query.columns.each_with_index do |column, i|
140 pdf.Cell(col_width[i], row_height, column.caption, 1, 0, 'L', 1)
140 pdf.Cell(col_width[i], row_height, column.caption, 1, 0, 'L', 1)
141 end
141 end
142 pdf.Ln
142 pdf.Ln
143
143
144 # rows
144 # rows
145 pdf.SetFontStyle('',8)
145 pdf.SetFontStyle('',8)
146 pdf.SetFillColor(255, 255, 255)
146 pdf.SetFillColor(255, 255, 255)
147 group = false
147 previous_group = false
148 issues.each do |issue|
148 issues.each do |issue|
149 if query.grouped? && issue.send(query.group_by) != group
149 if query.grouped? && (group = query.group_by_column.value(issue)) != previous_group
150 group = issue.send(query.group_by)
151 pdf.SetFontStyle('B',9)
150 pdf.SetFontStyle('B',9)
152 pdf.Cell(277, row_height, "#{group.blank? ? 'None' : group.to_s}", 1, 1, 'L')
151 pdf.Cell(277, row_height,
152 (group.blank? ? 'None' : group.to_s) + " (#{@issue_count_by_group[group]})",
153 1, 1, 'L')
153 pdf.SetFontStyle('',8)
154 pdf.SetFontStyle('',8)
155 previous_group = group
154 end
156 end
155 pdf.Cell(15, row_height, issue.id.to_s, 1, 0, 'L', 1)
157 pdf.Cell(15, row_height, issue.id.to_s, 1, 0, 'L', 1)
156 query.columns.each_with_index do |column, i|
158 query.columns.each_with_index do |column, i|
157 s = if column.is_a?(QueryCustomFieldColumn)
159 s = if column.is_a?(QueryCustomFieldColumn)
158 cv = issue.custom_values.detect {|v| v.custom_field_id == column.custom_field.id}
160 cv = issue.custom_values.detect {|v| v.custom_field_id == column.custom_field.id}
159 show_value(cv)
161 show_value(cv)
160 else
162 else
161 value = issue.send(column.name)
163 value = issue.send(column.name)
162 if value.is_a?(Date)
164 if value.is_a?(Date)
163 format_date(value)
165 format_date(value)
164 elsif value.is_a?(Time)
166 elsif value.is_a?(Time)
165 format_time(value)
167 format_time(value)
166 else
168 else
167 value
169 value
168 end
170 end
169 end
171 end
170 pdf.Cell(col_width[i], row_height, s.to_s, 1, 0, 'L', 1)
172 pdf.Cell(col_width[i], row_height, s.to_s, 1, 0, 'L', 1)
171 end
173 end
172 pdf.Ln
174 pdf.Ln
173 end
175 end
174 if issues.size == Setting.issues_export_limit.to_i
176 if issues.size == Setting.issues_export_limit.to_i
175 pdf.SetFontStyle('B',10)
177 pdf.SetFontStyle('B',10)
176 pdf.Cell(0, row_height, '...')
178 pdf.Cell(0, row_height, '...')
177 end
179 end
178 pdf.Output
180 pdf.Output
179 end
181 end
180
182
181 # Returns a PDF string of a single issue
183 # Returns a PDF string of a single issue
182 def issue_to_pdf(issue)
184 def issue_to_pdf(issue)
183 pdf = IFPDF.new(current_language)
185 pdf = IFPDF.new(current_language)
184 pdf.SetTitle("#{issue.project} - ##{issue.tracker} #{issue.id}")
186 pdf.SetTitle("#{issue.project} - ##{issue.tracker} #{issue.id}")
185 pdf.AliasNbPages
187 pdf.AliasNbPages
186 pdf.footer_date = format_date(Date.today)
188 pdf.footer_date = format_date(Date.today)
187 pdf.AddPage
189 pdf.AddPage
188
190
189 pdf.SetFontStyle('B',11)
191 pdf.SetFontStyle('B',11)
190 pdf.Cell(190,10, "#{issue.project} - #{issue.tracker} # #{issue.id}: #{issue.subject}")
192 pdf.Cell(190,10, "#{issue.project} - #{issue.tracker} # #{issue.id}: #{issue.subject}")
191 pdf.Ln
193 pdf.Ln
192
194
193 y0 = pdf.GetY
195 y0 = pdf.GetY
194
196
195 pdf.SetFontStyle('B',9)
197 pdf.SetFontStyle('B',9)
196 pdf.Cell(35,5, l(:field_status) + ":","LT")
198 pdf.Cell(35,5, l(:field_status) + ":","LT")
197 pdf.SetFontStyle('',9)
199 pdf.SetFontStyle('',9)
198 pdf.Cell(60,5, issue.status.to_s,"RT")
200 pdf.Cell(60,5, issue.status.to_s,"RT")
199 pdf.SetFontStyle('B',9)
201 pdf.SetFontStyle('B',9)
200 pdf.Cell(35,5, l(:field_priority) + ":","LT")
202 pdf.Cell(35,5, l(:field_priority) + ":","LT")
201 pdf.SetFontStyle('',9)
203 pdf.SetFontStyle('',9)
202 pdf.Cell(60,5, issue.priority.to_s,"RT")
204 pdf.Cell(60,5, issue.priority.to_s,"RT")
203 pdf.Ln
205 pdf.Ln
204
206
205 pdf.SetFontStyle('B',9)
207 pdf.SetFontStyle('B',9)
206 pdf.Cell(35,5, l(:field_author) + ":","L")
208 pdf.Cell(35,5, l(:field_author) + ":","L")
207 pdf.SetFontStyle('',9)
209 pdf.SetFontStyle('',9)
208 pdf.Cell(60,5, issue.author.to_s,"R")
210 pdf.Cell(60,5, issue.author.to_s,"R")
209 pdf.SetFontStyle('B',9)
211 pdf.SetFontStyle('B',9)
210 pdf.Cell(35,5, l(:field_category) + ":","L")
212 pdf.Cell(35,5, l(:field_category) + ":","L")
211 pdf.SetFontStyle('',9)
213 pdf.SetFontStyle('',9)
212 pdf.Cell(60,5, issue.category.to_s,"R")
214 pdf.Cell(60,5, issue.category.to_s,"R")
213 pdf.Ln
215 pdf.Ln
214
216
215 pdf.SetFontStyle('B',9)
217 pdf.SetFontStyle('B',9)
216 pdf.Cell(35,5, l(:field_created_on) + ":","L")
218 pdf.Cell(35,5, l(:field_created_on) + ":","L")
217 pdf.SetFontStyle('',9)
219 pdf.SetFontStyle('',9)
218 pdf.Cell(60,5, format_date(issue.created_on),"R")
220 pdf.Cell(60,5, format_date(issue.created_on),"R")
219 pdf.SetFontStyle('B',9)
221 pdf.SetFontStyle('B',9)
220 pdf.Cell(35,5, l(:field_assigned_to) + ":","L")
222 pdf.Cell(35,5, l(:field_assigned_to) + ":","L")
221 pdf.SetFontStyle('',9)
223 pdf.SetFontStyle('',9)
222 pdf.Cell(60,5, issue.assigned_to.to_s,"R")
224 pdf.Cell(60,5, issue.assigned_to.to_s,"R")
223 pdf.Ln
225 pdf.Ln
224
226
225 pdf.SetFontStyle('B',9)
227 pdf.SetFontStyle('B',9)
226 pdf.Cell(35,5, l(:field_updated_on) + ":","LB")
228 pdf.Cell(35,5, l(:field_updated_on) + ":","LB")
227 pdf.SetFontStyle('',9)
229 pdf.SetFontStyle('',9)
228 pdf.Cell(60,5, format_date(issue.updated_on),"RB")
230 pdf.Cell(60,5, format_date(issue.updated_on),"RB")
229 pdf.SetFontStyle('B',9)
231 pdf.SetFontStyle('B',9)
230 pdf.Cell(35,5, l(:field_due_date) + ":","LB")
232 pdf.Cell(35,5, l(:field_due_date) + ":","LB")
231 pdf.SetFontStyle('',9)
233 pdf.SetFontStyle('',9)
232 pdf.Cell(60,5, format_date(issue.due_date),"RB")
234 pdf.Cell(60,5, format_date(issue.due_date),"RB")
233 pdf.Ln
235 pdf.Ln
234
236
235 for custom_value in issue.custom_field_values
237 for custom_value in issue.custom_field_values
236 pdf.SetFontStyle('B',9)
238 pdf.SetFontStyle('B',9)
237 pdf.Cell(35,5, custom_value.custom_field.name + ":","L")
239 pdf.Cell(35,5, custom_value.custom_field.name + ":","L")
238 pdf.SetFontStyle('',9)
240 pdf.SetFontStyle('',9)
239 pdf.MultiCell(155,5, (show_value custom_value),"R")
241 pdf.MultiCell(155,5, (show_value custom_value),"R")
240 end
242 end
241
243
242 pdf.SetFontStyle('B',9)
244 pdf.SetFontStyle('B',9)
243 pdf.Cell(35,5, l(:field_subject) + ":","LTB")
245 pdf.Cell(35,5, l(:field_subject) + ":","LTB")
244 pdf.SetFontStyle('',9)
246 pdf.SetFontStyle('',9)
245 pdf.Cell(155,5, issue.subject,"RTB")
247 pdf.Cell(155,5, issue.subject,"RTB")
246 pdf.Ln
248 pdf.Ln
247
249
248 pdf.SetFontStyle('B',9)
250 pdf.SetFontStyle('B',9)
249 pdf.Cell(35,5, l(:field_description) + ":")
251 pdf.Cell(35,5, l(:field_description) + ":")
250 pdf.SetFontStyle('',9)
252 pdf.SetFontStyle('',9)
251 pdf.MultiCell(155,5, @issue.description,"BR")
253 pdf.MultiCell(155,5, @issue.description,"BR")
252
254
253 pdf.Line(pdf.GetX, y0, pdf.GetX, pdf.GetY)
255 pdf.Line(pdf.GetX, y0, pdf.GetX, pdf.GetY)
254 pdf.Line(pdf.GetX, pdf.GetY, 170, pdf.GetY)
256 pdf.Line(pdf.GetX, pdf.GetY, 170, pdf.GetY)
255 pdf.Ln
257 pdf.Ln
256
258
257 if issue.changesets.any? && User.current.allowed_to?(:view_changesets, issue.project)
259 if issue.changesets.any? && User.current.allowed_to?(:view_changesets, issue.project)
258 pdf.SetFontStyle('B',9)
260 pdf.SetFontStyle('B',9)
259 pdf.Cell(190,5, l(:label_associated_revisions), "B")
261 pdf.Cell(190,5, l(:label_associated_revisions), "B")
260 pdf.Ln
262 pdf.Ln
261 for changeset in issue.changesets
263 for changeset in issue.changesets
262 pdf.SetFontStyle('B',8)
264 pdf.SetFontStyle('B',8)
263 pdf.Cell(190,5, format_time(changeset.committed_on) + " - " + changeset.author.to_s)
265 pdf.Cell(190,5, format_time(changeset.committed_on) + " - " + changeset.author.to_s)
264 pdf.Ln
266 pdf.Ln
265 unless changeset.comments.blank?
267 unless changeset.comments.blank?
266 pdf.SetFontStyle('',8)
268 pdf.SetFontStyle('',8)
267 pdf.MultiCell(190,5, changeset.comments)
269 pdf.MultiCell(190,5, changeset.comments)
268 end
270 end
269 pdf.Ln
271 pdf.Ln
270 end
272 end
271 end
273 end
272
274
273 pdf.SetFontStyle('B',9)
275 pdf.SetFontStyle('B',9)
274 pdf.Cell(190,5, l(:label_history), "B")
276 pdf.Cell(190,5, l(:label_history), "B")
275 pdf.Ln
277 pdf.Ln
276 for journal in issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
278 for journal in issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
277 pdf.SetFontStyle('B',8)
279 pdf.SetFontStyle('B',8)
278 pdf.Cell(190,5, format_time(journal.created_on) + " - " + journal.user.name)
280 pdf.Cell(190,5, format_time(journal.created_on) + " - " + journal.user.name)
279 pdf.Ln
281 pdf.Ln
280 pdf.SetFontStyle('I',8)
282 pdf.SetFontStyle('I',8)
281 for detail in journal.details
283 for detail in journal.details
282 pdf.Cell(190,5, "- " + show_detail(detail, true))
284 pdf.Cell(190,5, "- " + show_detail(detail, true))
283 pdf.Ln
285 pdf.Ln
284 end
286 end
285 if journal.notes?
287 if journal.notes?
286 pdf.SetFontStyle('',8)
288 pdf.SetFontStyle('',8)
287 pdf.MultiCell(190,5, journal.notes)
289 pdf.MultiCell(190,5, journal.notes)
288 end
290 end
289 pdf.Ln
291 pdf.Ln
290 end
292 end
291
293
292 if issue.attachments.any?
294 if issue.attachments.any?
293 pdf.SetFontStyle('B',9)
295 pdf.SetFontStyle('B',9)
294 pdf.Cell(190,5, l(:label_attachment_plural), "B")
296 pdf.Cell(190,5, l(:label_attachment_plural), "B")
295 pdf.Ln
297 pdf.Ln
296 for attachment in issue.attachments
298 for attachment in issue.attachments
297 pdf.SetFontStyle('',8)
299 pdf.SetFontStyle('',8)
298 pdf.Cell(80,5, attachment.filename)
300 pdf.Cell(80,5, attachment.filename)
299 pdf.Cell(20,5, number_to_human_size(attachment.filesize),0,0,"R")
301 pdf.Cell(20,5, number_to_human_size(attachment.filesize),0,0,"R")
300 pdf.Cell(25,5, format_date(attachment.created_on),0,0,"R")
302 pdf.Cell(25,5, format_date(attachment.created_on),0,0,"R")
301 pdf.Cell(65,5, attachment.author.name,0,0,"R")
303 pdf.Cell(65,5, attachment.author.name,0,0,"R")
302 pdf.Ln
304 pdf.Ln
303 end
305 end
304 end
306 end
305 pdf.Output
307 pdf.Output
306 end
308 end
307
309
308 # Returns a PDF string of a gantt chart
310 # Returns a PDF string of a gantt chart
309 def gantt_to_pdf(gantt, project)
311 def gantt_to_pdf(gantt, project)
310 pdf = IFPDF.new(current_language)
312 pdf = IFPDF.new(current_language)
311 pdf.SetTitle("#{l(:label_gantt)} #{project}")
313 pdf.SetTitle("#{l(:label_gantt)} #{project}")
312 pdf.AliasNbPages
314 pdf.AliasNbPages
313 pdf.footer_date = format_date(Date.today)
315 pdf.footer_date = format_date(Date.today)
314 pdf.AddPage("L")
316 pdf.AddPage("L")
315 pdf.SetFontStyle('B',12)
317 pdf.SetFontStyle('B',12)
316 pdf.SetX(15)
318 pdf.SetX(15)
317 pdf.Cell(70, 20, project.to_s)
319 pdf.Cell(70, 20, project.to_s)
318 pdf.Ln
320 pdf.Ln
319 pdf.SetFontStyle('B',9)
321 pdf.SetFontStyle('B',9)
320
322
321 subject_width = 70
323 subject_width = 70
322 header_heigth = 5
324 header_heigth = 5
323
325
324 headers_heigth = header_heigth
326 headers_heigth = header_heigth
325 show_weeks = false
327 show_weeks = false
326 show_days = false
328 show_days = false
327
329
328 if gantt.months < 7
330 if gantt.months < 7
329 show_weeks = true
331 show_weeks = true
330 headers_heigth = 2*header_heigth
332 headers_heigth = 2*header_heigth
331 if gantt.months < 3
333 if gantt.months < 3
332 show_days = true
334 show_days = true
333 headers_heigth = 3*header_heigth
335 headers_heigth = 3*header_heigth
334 end
336 end
335 end
337 end
336
338
337 g_width = 210
339 g_width = 210
338 zoom = (g_width) / (gantt.date_to - gantt.date_from + 1)
340 zoom = (g_width) / (gantt.date_to - gantt.date_from + 1)
339 g_height = 120
341 g_height = 120
340 t_height = g_height + headers_heigth
342 t_height = g_height + headers_heigth
341
343
342 y_start = pdf.GetY
344 y_start = pdf.GetY
343
345
344 # Months headers
346 # Months headers
345 month_f = gantt.date_from
347 month_f = gantt.date_from
346 left = subject_width
348 left = subject_width
347 height = header_heigth
349 height = header_heigth
348 gantt.months.times do
350 gantt.months.times do
349 width = ((month_f >> 1) - month_f) * zoom
351 width = ((month_f >> 1) - month_f) * zoom
350 pdf.SetY(y_start)
352 pdf.SetY(y_start)
351 pdf.SetX(left)
353 pdf.SetX(left)
352 pdf.Cell(width, height, "#{month_f.year}-#{month_f.month}", "LTR", 0, "C")
354 pdf.Cell(width, height, "#{month_f.year}-#{month_f.month}", "LTR", 0, "C")
353 left = left + width
355 left = left + width
354 month_f = month_f >> 1
356 month_f = month_f >> 1
355 end
357 end
356
358
357 # Weeks headers
359 # Weeks headers
358 if show_weeks
360 if show_weeks
359 left = subject_width
361 left = subject_width
360 height = header_heigth
362 height = header_heigth
361 if gantt.date_from.cwday == 1
363 if gantt.date_from.cwday == 1
362 # gantt.date_from is monday
364 # gantt.date_from is monday
363 week_f = gantt.date_from
365 week_f = gantt.date_from
364 else
366 else
365 # find next monday after gantt.date_from
367 # find next monday after gantt.date_from
366 week_f = gantt.date_from + (7 - gantt.date_from.cwday + 1)
368 week_f = gantt.date_from + (7 - gantt.date_from.cwday + 1)
367 width = (7 - gantt.date_from.cwday + 1) * zoom-1
369 width = (7 - gantt.date_from.cwday + 1) * zoom-1
368 pdf.SetY(y_start + header_heigth)
370 pdf.SetY(y_start + header_heigth)
369 pdf.SetX(left)
371 pdf.SetX(left)
370 pdf.Cell(width + 1, height, "", "LTR")
372 pdf.Cell(width + 1, height, "", "LTR")
371 left = left + width+1
373 left = left + width+1
372 end
374 end
373 while week_f <= gantt.date_to
375 while week_f <= gantt.date_to
374 width = (week_f + 6 <= gantt.date_to) ? 7 * zoom : (gantt.date_to - week_f + 1) * zoom
376 width = (week_f + 6 <= gantt.date_to) ? 7 * zoom : (gantt.date_to - week_f + 1) * zoom
375 pdf.SetY(y_start + header_heigth)
377 pdf.SetY(y_start + header_heigth)
376 pdf.SetX(left)
378 pdf.SetX(left)
377 pdf.Cell(width, height, (width >= 5 ? week_f.cweek.to_s : ""), "LTR", 0, "C")
379 pdf.Cell(width, height, (width >= 5 ? week_f.cweek.to_s : ""), "LTR", 0, "C")
378 left = left + width
380 left = left + width
379 week_f = week_f+7
381 week_f = week_f+7
380 end
382 end
381 end
383 end
382
384
383 # Days headers
385 # Days headers
384 if show_days
386 if show_days
385 left = subject_width
387 left = subject_width
386 height = header_heigth
388 height = header_heigth
387 wday = gantt.date_from.cwday
389 wday = gantt.date_from.cwday
388 pdf.SetFontStyle('B',7)
390 pdf.SetFontStyle('B',7)
389 (gantt.date_to - gantt.date_from + 1).to_i.times do
391 (gantt.date_to - gantt.date_from + 1).to_i.times do
390 width = zoom
392 width = zoom
391 pdf.SetY(y_start + 2 * header_heigth)
393 pdf.SetY(y_start + 2 * header_heigth)
392 pdf.SetX(left)
394 pdf.SetX(left)
393 pdf.Cell(width, height, day_name(wday).first, "LTR", 0, "C")
395 pdf.Cell(width, height, day_name(wday).first, "LTR", 0, "C")
394 left = left + width
396 left = left + width
395 wday = wday + 1
397 wday = wday + 1
396 wday = 1 if wday > 7
398 wday = 1 if wday > 7
397 end
399 end
398 end
400 end
399
401
400 pdf.SetY(y_start)
402 pdf.SetY(y_start)
401 pdf.SetX(15)
403 pdf.SetX(15)
402 pdf.Cell(subject_width+g_width-15, headers_heigth, "", 1)
404 pdf.Cell(subject_width+g_width-15, headers_heigth, "", 1)
403
405
404 # Tasks
406 # Tasks
405 top = headers_heigth + y_start
407 top = headers_heigth + y_start
406 pdf.SetFontStyle('B',7)
408 pdf.SetFontStyle('B',7)
407 gantt.events.each do |i|
409 gantt.events.each do |i|
408 pdf.SetY(top)
410 pdf.SetY(top)
409 pdf.SetX(15)
411 pdf.SetX(15)
410
412
411 if i.is_a? Issue
413 if i.is_a? Issue
412 pdf.Cell(subject_width-15, 5, "#{i.tracker} #{i.id}: #{i.subject}".sub(/^(.{30}[^\s]*\s).*$/, '\1 (...)'), "LR")
414 pdf.Cell(subject_width-15, 5, "#{i.tracker} #{i.id}: #{i.subject}".sub(/^(.{30}[^\s]*\s).*$/, '\1 (...)'), "LR")
413 else
415 else
414 pdf.Cell(subject_width-15, 5, "#{l(:label_version)}: #{i.name}", "LR")
416 pdf.Cell(subject_width-15, 5, "#{l(:label_version)}: #{i.name}", "LR")
415 end
417 end
416
418
417 pdf.SetY(top)
419 pdf.SetY(top)
418 pdf.SetX(subject_width)
420 pdf.SetX(subject_width)
419 pdf.Cell(g_width, 5, "", "LR")
421 pdf.Cell(g_width, 5, "", "LR")
420
422
421 pdf.SetY(top+1.5)
423 pdf.SetY(top+1.5)
422
424
423 if i.is_a? Issue
425 if i.is_a? Issue
424 i_start_date = (i.start_date >= gantt.date_from ? i.start_date : gantt.date_from )
426 i_start_date = (i.start_date >= gantt.date_from ? i.start_date : gantt.date_from )
425 i_end_date = (i.due_before <= gantt.date_to ? i.due_before : gantt.date_to )
427 i_end_date = (i.due_before <= gantt.date_to ? i.due_before : gantt.date_to )
426
428
427 i_done_date = i.start_date + ((i.due_before - i.start_date+1)*i.done_ratio/100).floor
429 i_done_date = i.start_date + ((i.due_before - i.start_date+1)*i.done_ratio/100).floor
428 i_done_date = (i_done_date <= gantt.date_from ? gantt.date_from : i_done_date )
430 i_done_date = (i_done_date <= gantt.date_from ? gantt.date_from : i_done_date )
429 i_done_date = (i_done_date >= gantt.date_to ? gantt.date_to : i_done_date )
431 i_done_date = (i_done_date >= gantt.date_to ? gantt.date_to : i_done_date )
430
432
431 i_late_date = [i_end_date, Date.today].min if i_start_date < Date.today
433 i_late_date = [i_end_date, Date.today].min if i_start_date < Date.today
432
434
433 i_left = ((i_start_date - gantt.date_from)*zoom)
435 i_left = ((i_start_date - gantt.date_from)*zoom)
434 i_width = ((i_end_date - i_start_date + 1)*zoom)
436 i_width = ((i_end_date - i_start_date + 1)*zoom)
435 d_width = ((i_done_date - i_start_date)*zoom)
437 d_width = ((i_done_date - i_start_date)*zoom)
436 l_width = ((i_late_date - i_start_date+1)*zoom) if i_late_date
438 l_width = ((i_late_date - i_start_date+1)*zoom) if i_late_date
437 l_width ||= 0
439 l_width ||= 0
438
440
439 pdf.SetX(subject_width + i_left)
441 pdf.SetX(subject_width + i_left)
440 pdf.SetFillColor(200,200,200)
442 pdf.SetFillColor(200,200,200)
441 pdf.Cell(i_width, 2, "", 0, 0, "", 1)
443 pdf.Cell(i_width, 2, "", 0, 0, "", 1)
442
444
443 if l_width > 0
445 if l_width > 0
444 pdf.SetY(top+1.5)
446 pdf.SetY(top+1.5)
445 pdf.SetX(subject_width + i_left)
447 pdf.SetX(subject_width + i_left)
446 pdf.SetFillColor(255,100,100)
448 pdf.SetFillColor(255,100,100)
447 pdf.Cell(l_width, 2, "", 0, 0, "", 1)
449 pdf.Cell(l_width, 2, "", 0, 0, "", 1)
448 end
450 end
449 if d_width > 0
451 if d_width > 0
450 pdf.SetY(top+1.5)
452 pdf.SetY(top+1.5)
451 pdf.SetX(subject_width + i_left)
453 pdf.SetX(subject_width + i_left)
452 pdf.SetFillColor(100,100,255)
454 pdf.SetFillColor(100,100,255)
453 pdf.Cell(d_width, 2, "", 0, 0, "", 1)
455 pdf.Cell(d_width, 2, "", 0, 0, "", 1)
454 end
456 end
455
457
456 pdf.SetY(top+1.5)
458 pdf.SetY(top+1.5)
457 pdf.SetX(subject_width + i_left + i_width)
459 pdf.SetX(subject_width + i_left + i_width)
458 pdf.Cell(30, 2, "#{i.status} #{i.done_ratio}%")
460 pdf.Cell(30, 2, "#{i.status} #{i.done_ratio}%")
459 else
461 else
460 i_left = ((i.start_date - gantt.date_from)*zoom)
462 i_left = ((i.start_date - gantt.date_from)*zoom)
461
463
462 pdf.SetX(subject_width + i_left)
464 pdf.SetX(subject_width + i_left)
463 pdf.SetFillColor(50,200,50)
465 pdf.SetFillColor(50,200,50)
464 pdf.Cell(2, 2, "", 0, 0, "", 1)
466 pdf.Cell(2, 2, "", 0, 0, "", 1)
465
467
466 pdf.SetY(top+1.5)
468 pdf.SetY(top+1.5)
467 pdf.SetX(subject_width + i_left + 3)
469 pdf.SetX(subject_width + i_left + 3)
468 pdf.Cell(30, 2, "#{i.name}")
470 pdf.Cell(30, 2, "#{i.name}")
469 end
471 end
470
472
471 top = top + 5
473 top = top + 5
472 pdf.SetDrawColor(200, 200, 200)
474 pdf.SetDrawColor(200, 200, 200)
473 pdf.Line(15, top, subject_width+g_width, top)
475 pdf.Line(15, top, subject_width+g_width, top)
474 if pdf.GetY() > 180
476 if pdf.GetY() > 180
475 pdf.AddPage("L")
477 pdf.AddPage("L")
476 top = 20
478 top = 20
477 pdf.Line(15, top, subject_width+g_width, top)
479 pdf.Line(15, top, subject_width+g_width, top)
478 end
480 end
479 pdf.SetDrawColor(0, 0, 0)
481 pdf.SetDrawColor(0, 0, 0)
480 end
482 end
481
483
482 pdf.Line(15, top, subject_width+g_width, top)
484 pdf.Line(15, top, subject_width+g_width, top)
483 pdf.Output
485 pdf.Output
484 end
486 end
485 end
487 end
486 end
488 end
487 end
489 end
@@ -1,1337 +1,1345
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.dirname(__FILE__) + '/../test_helper'
18 require File.dirname(__FILE__) + '/../test_helper'
19 require 'issues_controller'
19 require 'issues_controller'
20
20
21 # Re-raise errors caught by the controller.
21 # Re-raise errors caught by the controller.
22 class IssuesController; def rescue_action(e) raise e end; end
22 class IssuesController; def rescue_action(e) raise e end; end
23
23
24 class IssuesControllerTest < ActionController::TestCase
24 class IssuesControllerTest < ActionController::TestCase
25 fixtures :projects,
25 fixtures :projects,
26 :users,
26 :users,
27 :roles,
27 :roles,
28 :members,
28 :members,
29 :member_roles,
29 :member_roles,
30 :issues,
30 :issues,
31 :issue_statuses,
31 :issue_statuses,
32 :versions,
32 :versions,
33 :trackers,
33 :trackers,
34 :projects_trackers,
34 :projects_trackers,
35 :issue_categories,
35 :issue_categories,
36 :enabled_modules,
36 :enabled_modules,
37 :enumerations,
37 :enumerations,
38 :attachments,
38 :attachments,
39 :workflows,
39 :workflows,
40 :custom_fields,
40 :custom_fields,
41 :custom_values,
41 :custom_values,
42 :custom_fields_projects,
42 :custom_fields_projects,
43 :custom_fields_trackers,
43 :custom_fields_trackers,
44 :time_entries,
44 :time_entries,
45 :journals,
45 :journals,
46 :journal_details,
46 :journal_details,
47 :queries
47 :queries
48
48
49 def setup
49 def setup
50 @controller = IssuesController.new
50 @controller = IssuesController.new
51 @request = ActionController::TestRequest.new
51 @request = ActionController::TestRequest.new
52 @response = ActionController::TestResponse.new
52 @response = ActionController::TestResponse.new
53 User.current = nil
53 User.current = nil
54 end
54 end
55
55
56 def test_index_routing
56 def test_index_routing
57 assert_routing(
57 assert_routing(
58 {:method => :get, :path => '/issues'},
58 {:method => :get, :path => '/issues'},
59 :controller => 'issues', :action => 'index'
59 :controller => 'issues', :action => 'index'
60 )
60 )
61 end
61 end
62
62
63 def test_index
63 def test_index
64 Setting.default_language = 'en'
64 Setting.default_language = 'en'
65
65
66 get :index
66 get :index
67 assert_response :success
67 assert_response :success
68 assert_template 'index.rhtml'
68 assert_template 'index.rhtml'
69 assert_not_nil assigns(:issues)
69 assert_not_nil assigns(:issues)
70 assert_nil assigns(:project)
70 assert_nil assigns(:project)
71 assert_tag :tag => 'a', :content => /Can't print recipes/
71 assert_tag :tag => 'a', :content => /Can't print recipes/
72 assert_tag :tag => 'a', :content => /Subproject issue/
72 assert_tag :tag => 'a', :content => /Subproject issue/
73 # private projects hidden
73 # private projects hidden
74 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
74 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
75 assert_no_tag :tag => 'a', :content => /Issue on project 2/
75 assert_no_tag :tag => 'a', :content => /Issue on project 2/
76 # project column
76 # project column
77 assert_tag :tag => 'th', :content => /Project/
77 assert_tag :tag => 'th', :content => /Project/
78 end
78 end
79
79
80 def test_index_should_not_list_issues_when_module_disabled
80 def test_index_should_not_list_issues_when_module_disabled
81 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
81 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
82 get :index
82 get :index
83 assert_response :success
83 assert_response :success
84 assert_template 'index.rhtml'
84 assert_template 'index.rhtml'
85 assert_not_nil assigns(:issues)
85 assert_not_nil assigns(:issues)
86 assert_nil assigns(:project)
86 assert_nil assigns(:project)
87 assert_no_tag :tag => 'a', :content => /Can't print recipes/
87 assert_no_tag :tag => 'a', :content => /Can't print recipes/
88 assert_tag :tag => 'a', :content => /Subproject issue/
88 assert_tag :tag => 'a', :content => /Subproject issue/
89 end
89 end
90
90
91 def test_index_with_project_routing
91 def test_index_with_project_routing
92 assert_routing(
92 assert_routing(
93 {:method => :get, :path => '/projects/23/issues'},
93 {:method => :get, :path => '/projects/23/issues'},
94 :controller => 'issues', :action => 'index', :project_id => '23'
94 :controller => 'issues', :action => 'index', :project_id => '23'
95 )
95 )
96 end
96 end
97
97
98 def test_index_should_not_list_issues_when_module_disabled
98 def test_index_should_not_list_issues_when_module_disabled
99 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
99 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
100 get :index
100 get :index
101 assert_response :success
101 assert_response :success
102 assert_template 'index.rhtml'
102 assert_template 'index.rhtml'
103 assert_not_nil assigns(:issues)
103 assert_not_nil assigns(:issues)
104 assert_nil assigns(:project)
104 assert_nil assigns(:project)
105 assert_no_tag :tag => 'a', :content => /Can't print recipes/
105 assert_no_tag :tag => 'a', :content => /Can't print recipes/
106 assert_tag :tag => 'a', :content => /Subproject issue/
106 assert_tag :tag => 'a', :content => /Subproject issue/
107 end
107 end
108
108
109 def test_index_with_project_routing
109 def test_index_with_project_routing
110 assert_routing(
110 assert_routing(
111 {:method => :get, :path => 'projects/23/issues'},
111 {:method => :get, :path => 'projects/23/issues'},
112 :controller => 'issues', :action => 'index', :project_id => '23'
112 :controller => 'issues', :action => 'index', :project_id => '23'
113 )
113 )
114 end
114 end
115
115
116 def test_index_with_project
116 def test_index_with_project
117 Setting.display_subprojects_issues = 0
117 Setting.display_subprojects_issues = 0
118 get :index, :project_id => 1
118 get :index, :project_id => 1
119 assert_response :success
119 assert_response :success
120 assert_template 'index.rhtml'
120 assert_template 'index.rhtml'
121 assert_not_nil assigns(:issues)
121 assert_not_nil assigns(:issues)
122 assert_tag :tag => 'a', :content => /Can't print recipes/
122 assert_tag :tag => 'a', :content => /Can't print recipes/
123 assert_no_tag :tag => 'a', :content => /Subproject issue/
123 assert_no_tag :tag => 'a', :content => /Subproject issue/
124 end
124 end
125
125
126 def test_index_with_project_and_subprojects
126 def test_index_with_project_and_subprojects
127 Setting.display_subprojects_issues = 1
127 Setting.display_subprojects_issues = 1
128 get :index, :project_id => 1
128 get :index, :project_id => 1
129 assert_response :success
129 assert_response :success
130 assert_template 'index.rhtml'
130 assert_template 'index.rhtml'
131 assert_not_nil assigns(:issues)
131 assert_not_nil assigns(:issues)
132 assert_tag :tag => 'a', :content => /Can't print recipes/
132 assert_tag :tag => 'a', :content => /Can't print recipes/
133 assert_tag :tag => 'a', :content => /Subproject issue/
133 assert_tag :tag => 'a', :content => /Subproject issue/
134 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
134 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
135 end
135 end
136
136
137 def test_index_with_project_and_subprojects_should_show_private_subprojects
137 def test_index_with_project_and_subprojects_should_show_private_subprojects
138 @request.session[:user_id] = 2
138 @request.session[:user_id] = 2
139 Setting.display_subprojects_issues = 1
139 Setting.display_subprojects_issues = 1
140 get :index, :project_id => 1
140 get :index, :project_id => 1
141 assert_response :success
141 assert_response :success
142 assert_template 'index.rhtml'
142 assert_template 'index.rhtml'
143 assert_not_nil assigns(:issues)
143 assert_not_nil assigns(:issues)
144 assert_tag :tag => 'a', :content => /Can't print recipes/
144 assert_tag :tag => 'a', :content => /Can't print recipes/
145 assert_tag :tag => 'a', :content => /Subproject issue/
145 assert_tag :tag => 'a', :content => /Subproject issue/
146 assert_tag :tag => 'a', :content => /Issue of a private subproject/
146 assert_tag :tag => 'a', :content => /Issue of a private subproject/
147 end
147 end
148
148
149 def test_index_with_project_routing_formatted
149 def test_index_with_project_routing_formatted
150 assert_routing(
150 assert_routing(
151 {:method => :get, :path => 'projects/23/issues.pdf'},
151 {:method => :get, :path => 'projects/23/issues.pdf'},
152 :controller => 'issues', :action => 'index', :project_id => '23', :format => 'pdf'
152 :controller => 'issues', :action => 'index', :project_id => '23', :format => 'pdf'
153 )
153 )
154 assert_routing(
154 assert_routing(
155 {:method => :get, :path => 'projects/23/issues.atom'},
155 {:method => :get, :path => 'projects/23/issues.atom'},
156 :controller => 'issues', :action => 'index', :project_id => '23', :format => 'atom'
156 :controller => 'issues', :action => 'index', :project_id => '23', :format => 'atom'
157 )
157 )
158 end
158 end
159
159
160 def test_index_with_project_and_filter
160 def test_index_with_project_and_filter
161 get :index, :project_id => 1, :set_filter => 1
161 get :index, :project_id => 1, :set_filter => 1
162 assert_response :success
162 assert_response :success
163 assert_template 'index.rhtml'
163 assert_template 'index.rhtml'
164 assert_not_nil assigns(:issues)
164 assert_not_nil assigns(:issues)
165 end
165 end
166
166
167 def test_index_with_query
167 def test_index_with_query
168 get :index, :project_id => 1, :query_id => 5
168 get :index, :project_id => 1, :query_id => 5
169 assert_response :success
169 assert_response :success
170 assert_template 'index.rhtml'
170 assert_template 'index.rhtml'
171 assert_not_nil assigns(:issues)
171 assert_not_nil assigns(:issues)
172 assert_nil assigns(:issue_count_by_group)
172 assert_nil assigns(:issue_count_by_group)
173 end
173 end
174
174
175 def test_index_with_query_grouped_by_tracker
175 def test_index_with_query_grouped_by_tracker
176 get :index, :project_id => 1, :query_id => 6
176 get :index, :project_id => 1, :query_id => 6
177 assert_response :success
177 assert_response :success
178 assert_template 'index.rhtml'
178 assert_template 'index.rhtml'
179 assert_not_nil assigns(:issues)
179 assert_not_nil assigns(:issues)
180 assert_not_nil assigns(:issue_count_by_group)
180 assert_not_nil assigns(:issue_count_by_group)
181 end
181 end
182
182
183 def test_index_with_query_grouped_by_list_custom_field
183 def test_index_with_query_grouped_by_list_custom_field
184 get :index, :project_id => 1, :query_id => 9
184 get :index, :project_id => 1, :query_id => 9
185 assert_response :success
185 assert_response :success
186 assert_template 'index.rhtml'
186 assert_template 'index.rhtml'
187 assert_not_nil assigns(:issues)
187 assert_not_nil assigns(:issues)
188 assert_not_nil assigns(:issue_count_by_group)
188 assert_not_nil assigns(:issue_count_by_group)
189 end
189 end
190
190
191 def test_index_sort_by_field_not_included_in_columns
191 def test_index_sort_by_field_not_included_in_columns
192 Setting.issue_list_default_columns = %w(subject author)
192 Setting.issue_list_default_columns = %w(subject author)
193 get :index, :sort => 'tracker'
193 get :index, :sort => 'tracker'
194 end
194 end
195
195
196 def test_index_csv_with_project
196 def test_index_csv_with_project
197 Setting.default_language = 'en'
197 Setting.default_language = 'en'
198
198
199 get :index, :format => 'csv'
199 get :index, :format => 'csv'
200 assert_response :success
200 assert_response :success
201 assert_not_nil assigns(:issues)
201 assert_not_nil assigns(:issues)
202 assert_equal 'text/csv', @response.content_type
202 assert_equal 'text/csv', @response.content_type
203 assert @response.body.starts_with?("#,")
203 assert @response.body.starts_with?("#,")
204
204
205 get :index, :project_id => 1, :format => 'csv'
205 get :index, :project_id => 1, :format => 'csv'
206 assert_response :success
206 assert_response :success
207 assert_not_nil assigns(:issues)
207 assert_not_nil assigns(:issues)
208 assert_equal 'text/csv', @response.content_type
208 assert_equal 'text/csv', @response.content_type
209 end
209 end
210
210
211 def test_index_formatted
211 def test_index_formatted
212 assert_routing(
212 assert_routing(
213 {:method => :get, :path => 'issues.pdf'},
213 {:method => :get, :path => 'issues.pdf'},
214 :controller => 'issues', :action => 'index', :format => 'pdf'
214 :controller => 'issues', :action => 'index', :format => 'pdf'
215 )
215 )
216 assert_routing(
216 assert_routing(
217 {:method => :get, :path => 'issues.atom'},
217 {:method => :get, :path => 'issues.atom'},
218 :controller => 'issues', :action => 'index', :format => 'atom'
218 :controller => 'issues', :action => 'index', :format => 'atom'
219 )
219 )
220 end
220 end
221
221
222 def test_index_pdf
222 def test_index_pdf
223 get :index, :format => 'pdf'
223 get :index, :format => 'pdf'
224 assert_response :success
224 assert_response :success
225 assert_not_nil assigns(:issues)
225 assert_not_nil assigns(:issues)
226 assert_equal 'application/pdf', @response.content_type
226 assert_equal 'application/pdf', @response.content_type
227
227
228 get :index, :project_id => 1, :format => 'pdf'
228 get :index, :project_id => 1, :format => 'pdf'
229 assert_response :success
229 assert_response :success
230 assert_not_nil assigns(:issues)
230 assert_not_nil assigns(:issues)
231 assert_equal 'application/pdf', @response.content_type
231 assert_equal 'application/pdf', @response.content_type
232
232
233 get :index, :project_id => 1, :query_id => 6, :format => 'pdf'
233 get :index, :project_id => 1, :query_id => 6, :format => 'pdf'
234 assert_response :success
234 assert_response :success
235 assert_not_nil assigns(:issues)
235 assert_not_nil assigns(:issues)
236 assert_equal 'application/pdf', @response.content_type
236 assert_equal 'application/pdf', @response.content_type
237 end
237 end
238
238
239 def test_index_pdf_with_query_grouped_by_list_custom_field
240 get :index, :project_id => 1, :query_id => 9, :format => 'pdf'
241 assert_response :success
242 assert_not_nil assigns(:issues)
243 assert_not_nil assigns(:issue_count_by_group)
244 assert_equal 'application/pdf', @response.content_type
245 end
246
239 def test_index_sort
247 def test_index_sort
240 get :index, :sort => 'tracker,id:desc'
248 get :index, :sort => 'tracker,id:desc'
241 assert_response :success
249 assert_response :success
242
250
243 sort_params = @request.session['issues_index_sort']
251 sort_params = @request.session['issues_index_sort']
244 assert sort_params.is_a?(String)
252 assert sort_params.is_a?(String)
245 assert_equal 'tracker,id:desc', sort_params
253 assert_equal 'tracker,id:desc', sort_params
246
254
247 issues = assigns(:issues)
255 issues = assigns(:issues)
248 assert_not_nil issues
256 assert_not_nil issues
249 assert !issues.empty?
257 assert !issues.empty?
250 assert_equal issues.sort {|a,b| a.tracker == b.tracker ? b.id <=> a.id : a.tracker <=> b.tracker }.collect(&:id), issues.collect(&:id)
258 assert_equal issues.sort {|a,b| a.tracker == b.tracker ? b.id <=> a.id : a.tracker <=> b.tracker }.collect(&:id), issues.collect(&:id)
251 end
259 end
252
260
253 def test_index_with_columns
261 def test_index_with_columns
254 columns = ['tracker', 'subject', 'assigned_to']
262 columns = ['tracker', 'subject', 'assigned_to']
255 get :index, :set_filter => 1, :query => { 'column_names' => columns}
263 get :index, :set_filter => 1, :query => { 'column_names' => columns}
256 assert_response :success
264 assert_response :success
257
265
258 # query should use specified columns
266 # query should use specified columns
259 query = assigns(:query)
267 query = assigns(:query)
260 assert_kind_of Query, query
268 assert_kind_of Query, query
261 assert_equal columns, query.column_names.map(&:to_s)
269 assert_equal columns, query.column_names.map(&:to_s)
262
270
263 # columns should be stored in session
271 # columns should be stored in session
264 assert_kind_of Hash, session[:query]
272 assert_kind_of Hash, session[:query]
265 assert_kind_of Array, session[:query][:column_names]
273 assert_kind_of Array, session[:query][:column_names]
266 assert_equal columns, session[:query][:column_names].map(&:to_s)
274 assert_equal columns, session[:query][:column_names].map(&:to_s)
267 end
275 end
268
276
269 def test_gantt
277 def test_gantt
270 get :gantt, :project_id => 1
278 get :gantt, :project_id => 1
271 assert_response :success
279 assert_response :success
272 assert_template 'gantt.rhtml'
280 assert_template 'gantt.rhtml'
273 assert_not_nil assigns(:gantt)
281 assert_not_nil assigns(:gantt)
274 events = assigns(:gantt).events
282 events = assigns(:gantt).events
275 assert_not_nil events
283 assert_not_nil events
276 # Issue with start and due dates
284 # Issue with start and due dates
277 i = Issue.find(1)
285 i = Issue.find(1)
278 assert_not_nil i.due_date
286 assert_not_nil i.due_date
279 assert events.include?(Issue.find(1))
287 assert events.include?(Issue.find(1))
280 # Issue with without due date but targeted to a version with date
288 # Issue with without due date but targeted to a version with date
281 i = Issue.find(2)
289 i = Issue.find(2)
282 assert_nil i.due_date
290 assert_nil i.due_date
283 assert events.include?(i)
291 assert events.include?(i)
284 end
292 end
285
293
286 def test_cross_project_gantt
294 def test_cross_project_gantt
287 get :gantt
295 get :gantt
288 assert_response :success
296 assert_response :success
289 assert_template 'gantt.rhtml'
297 assert_template 'gantt.rhtml'
290 assert_not_nil assigns(:gantt)
298 assert_not_nil assigns(:gantt)
291 events = assigns(:gantt).events
299 events = assigns(:gantt).events
292 assert_not_nil events
300 assert_not_nil events
293 end
301 end
294
302
295 def test_gantt_export_to_pdf
303 def test_gantt_export_to_pdf
296 get :gantt, :project_id => 1, :format => 'pdf'
304 get :gantt, :project_id => 1, :format => 'pdf'
297 assert_response :success
305 assert_response :success
298 assert_equal 'application/pdf', @response.content_type
306 assert_equal 'application/pdf', @response.content_type
299 assert @response.body.starts_with?('%PDF')
307 assert @response.body.starts_with?('%PDF')
300 assert_not_nil assigns(:gantt)
308 assert_not_nil assigns(:gantt)
301 end
309 end
302
310
303 def test_cross_project_gantt_export_to_pdf
311 def test_cross_project_gantt_export_to_pdf
304 get :gantt, :format => 'pdf'
312 get :gantt, :format => 'pdf'
305 assert_response :success
313 assert_response :success
306 assert_equal 'application/pdf', @response.content_type
314 assert_equal 'application/pdf', @response.content_type
307 assert @response.body.starts_with?('%PDF')
315 assert @response.body.starts_with?('%PDF')
308 assert_not_nil assigns(:gantt)
316 assert_not_nil assigns(:gantt)
309 end
317 end
310
318
311 if Object.const_defined?(:Magick)
319 if Object.const_defined?(:Magick)
312 def test_gantt_image
320 def test_gantt_image
313 get :gantt, :project_id => 1, :format => 'png'
321 get :gantt, :project_id => 1, :format => 'png'
314 assert_response :success
322 assert_response :success
315 assert_equal 'image/png', @response.content_type
323 assert_equal 'image/png', @response.content_type
316 end
324 end
317 else
325 else
318 puts "RMagick not installed. Skipping tests !!!"
326 puts "RMagick not installed. Skipping tests !!!"
319 end
327 end
320
328
321 def test_calendar
329 def test_calendar
322 get :calendar, :project_id => 1
330 get :calendar, :project_id => 1
323 assert_response :success
331 assert_response :success
324 assert_template 'calendar'
332 assert_template 'calendar'
325 assert_not_nil assigns(:calendar)
333 assert_not_nil assigns(:calendar)
326 end
334 end
327
335
328 def test_cross_project_calendar
336 def test_cross_project_calendar
329 get :calendar
337 get :calendar
330 assert_response :success
338 assert_response :success
331 assert_template 'calendar'
339 assert_template 'calendar'
332 assert_not_nil assigns(:calendar)
340 assert_not_nil assigns(:calendar)
333 end
341 end
334
342
335 def test_changes
343 def test_changes
336 get :changes, :project_id => 1
344 get :changes, :project_id => 1
337 assert_response :success
345 assert_response :success
338 assert_not_nil assigns(:journals)
346 assert_not_nil assigns(:journals)
339 assert_equal 'application/atom+xml', @response.content_type
347 assert_equal 'application/atom+xml', @response.content_type
340 end
348 end
341
349
342 def test_show_routing
350 def test_show_routing
343 assert_routing(
351 assert_routing(
344 {:method => :get, :path => '/issues/64'},
352 {:method => :get, :path => '/issues/64'},
345 :controller => 'issues', :action => 'show', :id => '64'
353 :controller => 'issues', :action => 'show', :id => '64'
346 )
354 )
347 end
355 end
348
356
349 def test_show_routing_formatted
357 def test_show_routing_formatted
350 assert_routing(
358 assert_routing(
351 {:method => :get, :path => '/issues/2332.pdf'},
359 {:method => :get, :path => '/issues/2332.pdf'},
352 :controller => 'issues', :action => 'show', :id => '2332', :format => 'pdf'
360 :controller => 'issues', :action => 'show', :id => '2332', :format => 'pdf'
353 )
361 )
354 assert_routing(
362 assert_routing(
355 {:method => :get, :path => '/issues/23123.atom'},
363 {:method => :get, :path => '/issues/23123.atom'},
356 :controller => 'issues', :action => 'show', :id => '23123', :format => 'atom'
364 :controller => 'issues', :action => 'show', :id => '23123', :format => 'atom'
357 )
365 )
358 end
366 end
359
367
360 def test_show_by_anonymous
368 def test_show_by_anonymous
361 get :show, :id => 1
369 get :show, :id => 1
362 assert_response :success
370 assert_response :success
363 assert_template 'show.rhtml'
371 assert_template 'show.rhtml'
364 assert_not_nil assigns(:issue)
372 assert_not_nil assigns(:issue)
365 assert_equal Issue.find(1), assigns(:issue)
373 assert_equal Issue.find(1), assigns(:issue)
366
374
367 # anonymous role is allowed to add a note
375 # anonymous role is allowed to add a note
368 assert_tag :tag => 'form',
376 assert_tag :tag => 'form',
369 :descendant => { :tag => 'fieldset',
377 :descendant => { :tag => 'fieldset',
370 :child => { :tag => 'legend',
378 :child => { :tag => 'legend',
371 :content => /Notes/ } }
379 :content => /Notes/ } }
372 end
380 end
373
381
374 def test_show_by_manager
382 def test_show_by_manager
375 @request.session[:user_id] = 2
383 @request.session[:user_id] = 2
376 get :show, :id => 1
384 get :show, :id => 1
377 assert_response :success
385 assert_response :success
378
386
379 assert_tag :tag => 'form',
387 assert_tag :tag => 'form',
380 :descendant => { :tag => 'fieldset',
388 :descendant => { :tag => 'fieldset',
381 :child => { :tag => 'legend',
389 :child => { :tag => 'legend',
382 :content => /Change properties/ } },
390 :content => /Change properties/ } },
383 :descendant => { :tag => 'fieldset',
391 :descendant => { :tag => 'fieldset',
384 :child => { :tag => 'legend',
392 :child => { :tag => 'legend',
385 :content => /Log time/ } },
393 :content => /Log time/ } },
386 :descendant => { :tag => 'fieldset',
394 :descendant => { :tag => 'fieldset',
387 :child => { :tag => 'legend',
395 :child => { :tag => 'legend',
388 :content => /Notes/ } }
396 :content => /Notes/ } }
389 end
397 end
390
398
391 def test_show_should_deny_anonymous_access_without_permission
399 def test_show_should_deny_anonymous_access_without_permission
392 Role.anonymous.remove_permission!(:view_issues)
400 Role.anonymous.remove_permission!(:view_issues)
393 get :show, :id => 1
401 get :show, :id => 1
394 assert_response :redirect
402 assert_response :redirect
395 end
403 end
396
404
397 def test_show_should_deny_non_member_access_without_permission
405 def test_show_should_deny_non_member_access_without_permission
398 Role.non_member.remove_permission!(:view_issues)
406 Role.non_member.remove_permission!(:view_issues)
399 @request.session[:user_id] = 9
407 @request.session[:user_id] = 9
400 get :show, :id => 1
408 get :show, :id => 1
401 assert_response 403
409 assert_response 403
402 end
410 end
403
411
404 def test_show_should_deny_member_access_without_permission
412 def test_show_should_deny_member_access_without_permission
405 Role.find(1).remove_permission!(:view_issues)
413 Role.find(1).remove_permission!(:view_issues)
406 @request.session[:user_id] = 2
414 @request.session[:user_id] = 2
407 get :show, :id => 1
415 get :show, :id => 1
408 assert_response 403
416 assert_response 403
409 end
417 end
410
418
411 def test_show_should_not_disclose_relations_to_invisible_issues
419 def test_show_should_not_disclose_relations_to_invisible_issues
412 Setting.cross_project_issue_relations = '1'
420 Setting.cross_project_issue_relations = '1'
413 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(2), :relation_type => 'relates')
421 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(2), :relation_type => 'relates')
414 # Relation to a private project issue
422 # Relation to a private project issue
415 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(4), :relation_type => 'relates')
423 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(4), :relation_type => 'relates')
416
424
417 get :show, :id => 1
425 get :show, :id => 1
418 assert_response :success
426 assert_response :success
419
427
420 assert_tag :div, :attributes => { :id => 'relations' },
428 assert_tag :div, :attributes => { :id => 'relations' },
421 :descendant => { :tag => 'a', :content => /#2$/ }
429 :descendant => { :tag => 'a', :content => /#2$/ }
422 assert_no_tag :div, :attributes => { :id => 'relations' },
430 assert_no_tag :div, :attributes => { :id => 'relations' },
423 :descendant => { :tag => 'a', :content => /#4$/ }
431 :descendant => { :tag => 'a', :content => /#4$/ }
424 end
432 end
425
433
426 def test_show_atom
434 def test_show_atom
427 get :show, :id => 2, :format => 'atom'
435 get :show, :id => 2, :format => 'atom'
428 assert_response :success
436 assert_response :success
429 assert_template 'changes.rxml'
437 assert_template 'changes.rxml'
430 # Inline image
438 # Inline image
431 assert @response.body.include?("&lt;img src=\"http://test.host/attachments/download/10\" alt=\"\" /&gt;"), "Body did not match. Body: #{@response.body}"
439 assert @response.body.include?("&lt;img src=\"http://test.host/attachments/download/10\" alt=\"\" /&gt;"), "Body did not match. Body: #{@response.body}"
432 end
440 end
433
441
434 def test_new_routing
442 def test_new_routing
435 assert_routing(
443 assert_routing(
436 {:method => :get, :path => '/projects/1/issues/new'},
444 {:method => :get, :path => '/projects/1/issues/new'},
437 :controller => 'issues', :action => 'new', :project_id => '1'
445 :controller => 'issues', :action => 'new', :project_id => '1'
438 )
446 )
439 assert_recognizes(
447 assert_recognizes(
440 {:controller => 'issues', :action => 'new', :project_id => '1'},
448 {:controller => 'issues', :action => 'new', :project_id => '1'},
441 {:method => :post, :path => '/projects/1/issues'}
449 {:method => :post, :path => '/projects/1/issues'}
442 )
450 )
443 end
451 end
444
452
445 def test_show_export_to_pdf
453 def test_show_export_to_pdf
446 get :show, :id => 3, :format => 'pdf'
454 get :show, :id => 3, :format => 'pdf'
447 assert_response :success
455 assert_response :success
448 assert_equal 'application/pdf', @response.content_type
456 assert_equal 'application/pdf', @response.content_type
449 assert @response.body.starts_with?('%PDF')
457 assert @response.body.starts_with?('%PDF')
450 assert_not_nil assigns(:issue)
458 assert_not_nil assigns(:issue)
451 end
459 end
452
460
453 def test_get_new
461 def test_get_new
454 @request.session[:user_id] = 2
462 @request.session[:user_id] = 2
455 get :new, :project_id => 1, :tracker_id => 1
463 get :new, :project_id => 1, :tracker_id => 1
456 assert_response :success
464 assert_response :success
457 assert_template 'new'
465 assert_template 'new'
458
466
459 assert_tag :tag => 'input', :attributes => { :name => 'issue[custom_field_values][2]',
467 assert_tag :tag => 'input', :attributes => { :name => 'issue[custom_field_values][2]',
460 :value => 'Default string' }
468 :value => 'Default string' }
461 end
469 end
462
470
463 def test_get_new_without_tracker_id
471 def test_get_new_without_tracker_id
464 @request.session[:user_id] = 2
472 @request.session[:user_id] = 2
465 get :new, :project_id => 1
473 get :new, :project_id => 1
466 assert_response :success
474 assert_response :success
467 assert_template 'new'
475 assert_template 'new'
468
476
469 issue = assigns(:issue)
477 issue = assigns(:issue)
470 assert_not_nil issue
478 assert_not_nil issue
471 assert_equal Project.find(1).trackers.first, issue.tracker
479 assert_equal Project.find(1).trackers.first, issue.tracker
472 end
480 end
473
481
474 def test_get_new_with_no_default_status_should_display_an_error
482 def test_get_new_with_no_default_status_should_display_an_error
475 @request.session[:user_id] = 2
483 @request.session[:user_id] = 2
476 IssueStatus.delete_all
484 IssueStatus.delete_all
477
485
478 get :new, :project_id => 1
486 get :new, :project_id => 1
479 assert_response 500
487 assert_response 500
480 assert_not_nil flash[:error]
488 assert_not_nil flash[:error]
481 assert_tag :tag => 'div', :attributes => { :class => /error/ },
489 assert_tag :tag => 'div', :attributes => { :class => /error/ },
482 :content => /No default issue/
490 :content => /No default issue/
483 end
491 end
484
492
485 def test_get_new_with_no_tracker_should_display_an_error
493 def test_get_new_with_no_tracker_should_display_an_error
486 @request.session[:user_id] = 2
494 @request.session[:user_id] = 2
487 Tracker.delete_all
495 Tracker.delete_all
488
496
489 get :new, :project_id => 1
497 get :new, :project_id => 1
490 assert_response 500
498 assert_response 500
491 assert_not_nil flash[:error]
499 assert_not_nil flash[:error]
492 assert_tag :tag => 'div', :attributes => { :class => /error/ },
500 assert_tag :tag => 'div', :attributes => { :class => /error/ },
493 :content => /No tracker/
501 :content => /No tracker/
494 end
502 end
495
503
496 def test_update_new_form
504 def test_update_new_form
497 @request.session[:user_id] = 2
505 @request.session[:user_id] = 2
498 xhr :post, :update_form, :project_id => 1,
506 xhr :post, :update_form, :project_id => 1,
499 :issue => {:tracker_id => 2,
507 :issue => {:tracker_id => 2,
500 :subject => 'This is the test_new issue',
508 :subject => 'This is the test_new issue',
501 :description => 'This is the description',
509 :description => 'This is the description',
502 :priority_id => 5}
510 :priority_id => 5}
503 assert_response :success
511 assert_response :success
504 assert_template 'attributes'
512 assert_template 'attributes'
505
513
506 issue = assigns(:issue)
514 issue = assigns(:issue)
507 assert_kind_of Issue, issue
515 assert_kind_of Issue, issue
508 assert_equal 1, issue.project_id
516 assert_equal 1, issue.project_id
509 assert_equal 2, issue.tracker_id
517 assert_equal 2, issue.tracker_id
510 assert_equal 'This is the test_new issue', issue.subject
518 assert_equal 'This is the test_new issue', issue.subject
511 end
519 end
512
520
513 def test_post_new
521 def test_post_new
514 @request.session[:user_id] = 2
522 @request.session[:user_id] = 2
515 assert_difference 'Issue.count' do
523 assert_difference 'Issue.count' do
516 post :new, :project_id => 1,
524 post :new, :project_id => 1,
517 :issue => {:tracker_id => 3,
525 :issue => {:tracker_id => 3,
518 :subject => 'This is the test_new issue',
526 :subject => 'This is the test_new issue',
519 :description => 'This is the description',
527 :description => 'This is the description',
520 :priority_id => 5,
528 :priority_id => 5,
521 :estimated_hours => '',
529 :estimated_hours => '',
522 :custom_field_values => {'2' => 'Value for field 2'}}
530 :custom_field_values => {'2' => 'Value for field 2'}}
523 end
531 end
524 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
532 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
525
533
526 issue = Issue.find_by_subject('This is the test_new issue')
534 issue = Issue.find_by_subject('This is the test_new issue')
527 assert_not_nil issue
535 assert_not_nil issue
528 assert_equal 2, issue.author_id
536 assert_equal 2, issue.author_id
529 assert_equal 3, issue.tracker_id
537 assert_equal 3, issue.tracker_id
530 assert_nil issue.estimated_hours
538 assert_nil issue.estimated_hours
531 v = issue.custom_values.find(:first, :conditions => {:custom_field_id => 2})
539 v = issue.custom_values.find(:first, :conditions => {:custom_field_id => 2})
532 assert_not_nil v
540 assert_not_nil v
533 assert_equal 'Value for field 2', v.value
541 assert_equal 'Value for field 2', v.value
534 end
542 end
535
543
536 def test_post_new_and_continue
544 def test_post_new_and_continue
537 @request.session[:user_id] = 2
545 @request.session[:user_id] = 2
538 post :new, :project_id => 1,
546 post :new, :project_id => 1,
539 :issue => {:tracker_id => 3,
547 :issue => {:tracker_id => 3,
540 :subject => 'This is first issue',
548 :subject => 'This is first issue',
541 :priority_id => 5},
549 :priority_id => 5},
542 :continue => ''
550 :continue => ''
543 assert_redirected_to :controller => 'issues', :action => 'new', :tracker_id => 3
551 assert_redirected_to :controller => 'issues', :action => 'new', :tracker_id => 3
544 end
552 end
545
553
546 def test_post_new_without_custom_fields_param
554 def test_post_new_without_custom_fields_param
547 @request.session[:user_id] = 2
555 @request.session[:user_id] = 2
548 assert_difference 'Issue.count' do
556 assert_difference 'Issue.count' do
549 post :new, :project_id => 1,
557 post :new, :project_id => 1,
550 :issue => {:tracker_id => 1,
558 :issue => {:tracker_id => 1,
551 :subject => 'This is the test_new issue',
559 :subject => 'This is the test_new issue',
552 :description => 'This is the description',
560 :description => 'This is the description',
553 :priority_id => 5}
561 :priority_id => 5}
554 end
562 end
555 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
563 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
556 end
564 end
557
565
558 def test_post_new_with_required_custom_field_and_without_custom_fields_param
566 def test_post_new_with_required_custom_field_and_without_custom_fields_param
559 field = IssueCustomField.find_by_name('Database')
567 field = IssueCustomField.find_by_name('Database')
560 field.update_attribute(:is_required, true)
568 field.update_attribute(:is_required, true)
561
569
562 @request.session[:user_id] = 2
570 @request.session[:user_id] = 2
563 post :new, :project_id => 1,
571 post :new, :project_id => 1,
564 :issue => {:tracker_id => 1,
572 :issue => {:tracker_id => 1,
565 :subject => 'This is the test_new issue',
573 :subject => 'This is the test_new issue',
566 :description => 'This is the description',
574 :description => 'This is the description',
567 :priority_id => 5}
575 :priority_id => 5}
568 assert_response :success
576 assert_response :success
569 assert_template 'new'
577 assert_template 'new'
570 issue = assigns(:issue)
578 issue = assigns(:issue)
571 assert_not_nil issue
579 assert_not_nil issue
572 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
580 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
573 end
581 end
574
582
575 def test_post_new_with_watchers
583 def test_post_new_with_watchers
576 @request.session[:user_id] = 2
584 @request.session[:user_id] = 2
577 ActionMailer::Base.deliveries.clear
585 ActionMailer::Base.deliveries.clear
578
586
579 assert_difference 'Watcher.count', 2 do
587 assert_difference 'Watcher.count', 2 do
580 post :new, :project_id => 1,
588 post :new, :project_id => 1,
581 :issue => {:tracker_id => 1,
589 :issue => {:tracker_id => 1,
582 :subject => 'This is a new issue with watchers',
590 :subject => 'This is a new issue with watchers',
583 :description => 'This is the description',
591 :description => 'This is the description',
584 :priority_id => 5,
592 :priority_id => 5,
585 :watcher_user_ids => ['2', '3']}
593 :watcher_user_ids => ['2', '3']}
586 end
594 end
587 issue = Issue.find_by_subject('This is a new issue with watchers')
595 issue = Issue.find_by_subject('This is a new issue with watchers')
588 assert_not_nil issue
596 assert_not_nil issue
589 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
597 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
590
598
591 # Watchers added
599 # Watchers added
592 assert_equal [2, 3], issue.watcher_user_ids.sort
600 assert_equal [2, 3], issue.watcher_user_ids.sort
593 assert issue.watched_by?(User.find(3))
601 assert issue.watched_by?(User.find(3))
594 # Watchers notified
602 # Watchers notified
595 mail = ActionMailer::Base.deliveries.last
603 mail = ActionMailer::Base.deliveries.last
596 assert_kind_of TMail::Mail, mail
604 assert_kind_of TMail::Mail, mail
597 assert [mail.bcc, mail.cc].flatten.include?(User.find(3).mail)
605 assert [mail.bcc, mail.cc].flatten.include?(User.find(3).mail)
598 end
606 end
599
607
600 def test_post_new_should_send_a_notification
608 def test_post_new_should_send_a_notification
601 ActionMailer::Base.deliveries.clear
609 ActionMailer::Base.deliveries.clear
602 @request.session[:user_id] = 2
610 @request.session[:user_id] = 2
603 assert_difference 'Issue.count' do
611 assert_difference 'Issue.count' do
604 post :new, :project_id => 1,
612 post :new, :project_id => 1,
605 :issue => {:tracker_id => 3,
613 :issue => {:tracker_id => 3,
606 :subject => 'This is the test_new issue',
614 :subject => 'This is the test_new issue',
607 :description => 'This is the description',
615 :description => 'This is the description',
608 :priority_id => 5,
616 :priority_id => 5,
609 :estimated_hours => '',
617 :estimated_hours => '',
610 :custom_field_values => {'2' => 'Value for field 2'}}
618 :custom_field_values => {'2' => 'Value for field 2'}}
611 end
619 end
612 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
620 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
613
621
614 assert_equal 1, ActionMailer::Base.deliveries.size
622 assert_equal 1, ActionMailer::Base.deliveries.size
615 end
623 end
616
624
617 def test_post_should_preserve_fields_values_on_validation_failure
625 def test_post_should_preserve_fields_values_on_validation_failure
618 @request.session[:user_id] = 2
626 @request.session[:user_id] = 2
619 post :new, :project_id => 1,
627 post :new, :project_id => 1,
620 :issue => {:tracker_id => 1,
628 :issue => {:tracker_id => 1,
621 # empty subject
629 # empty subject
622 :subject => '',
630 :subject => '',
623 :description => 'This is a description',
631 :description => 'This is a description',
624 :priority_id => 6,
632 :priority_id => 6,
625 :custom_field_values => {'1' => 'Oracle', '2' => 'Value for field 2'}}
633 :custom_field_values => {'1' => 'Oracle', '2' => 'Value for field 2'}}
626 assert_response :success
634 assert_response :success
627 assert_template 'new'
635 assert_template 'new'
628
636
629 assert_tag :textarea, :attributes => { :name => 'issue[description]' },
637 assert_tag :textarea, :attributes => { :name => 'issue[description]' },
630 :content => 'This is a description'
638 :content => 'This is a description'
631 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
639 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
632 :child => { :tag => 'option', :attributes => { :selected => 'selected',
640 :child => { :tag => 'option', :attributes => { :selected => 'selected',
633 :value => '6' },
641 :value => '6' },
634 :content => 'High' }
642 :content => 'High' }
635 # Custom fields
643 # Custom fields
636 assert_tag :select, :attributes => { :name => 'issue[custom_field_values][1]' },
644 assert_tag :select, :attributes => { :name => 'issue[custom_field_values][1]' },
637 :child => { :tag => 'option', :attributes => { :selected => 'selected',
645 :child => { :tag => 'option', :attributes => { :selected => 'selected',
638 :value => 'Oracle' },
646 :value => 'Oracle' },
639 :content => 'Oracle' }
647 :content => 'Oracle' }
640 assert_tag :input, :attributes => { :name => 'issue[custom_field_values][2]',
648 assert_tag :input, :attributes => { :name => 'issue[custom_field_values][2]',
641 :value => 'Value for field 2'}
649 :value => 'Value for field 2'}
642 end
650 end
643
651
644 def test_post_new_should_ignore_non_safe_attributes
652 def test_post_new_should_ignore_non_safe_attributes
645 @request.session[:user_id] = 2
653 @request.session[:user_id] = 2
646 assert_nothing_raised do
654 assert_nothing_raised do
647 post :new, :project_id => 1, :issue => { :tracker => "A param can not be a Tracker" }
655 post :new, :project_id => 1, :issue => { :tracker => "A param can not be a Tracker" }
648 end
656 end
649 end
657 end
650
658
651 def test_copy_routing
659 def test_copy_routing
652 assert_routing(
660 assert_routing(
653 {:method => :get, :path => '/projects/world_domination/issues/567/copy'},
661 {:method => :get, :path => '/projects/world_domination/issues/567/copy'},
654 :controller => 'issues', :action => 'new', :project_id => 'world_domination', :copy_from => '567'
662 :controller => 'issues', :action => 'new', :project_id => 'world_domination', :copy_from => '567'
655 )
663 )
656 end
664 end
657
665
658 def test_copy_issue
666 def test_copy_issue
659 @request.session[:user_id] = 2
667 @request.session[:user_id] = 2
660 get :new, :project_id => 1, :copy_from => 1
668 get :new, :project_id => 1, :copy_from => 1
661 assert_template 'new'
669 assert_template 'new'
662 assert_not_nil assigns(:issue)
670 assert_not_nil assigns(:issue)
663 orig = Issue.find(1)
671 orig = Issue.find(1)
664 assert_equal orig.subject, assigns(:issue).subject
672 assert_equal orig.subject, assigns(:issue).subject
665 end
673 end
666
674
667 def test_edit_routing
675 def test_edit_routing
668 assert_routing(
676 assert_routing(
669 {:method => :get, :path => '/issues/1/edit'},
677 {:method => :get, :path => '/issues/1/edit'},
670 :controller => 'issues', :action => 'edit', :id => '1'
678 :controller => 'issues', :action => 'edit', :id => '1'
671 )
679 )
672 assert_recognizes( #TODO: use a PUT on the issue URI isntead, need to adjust form
680 assert_recognizes( #TODO: use a PUT on the issue URI isntead, need to adjust form
673 {:controller => 'issues', :action => 'edit', :id => '1'},
681 {:controller => 'issues', :action => 'edit', :id => '1'},
674 {:method => :post, :path => '/issues/1/edit'}
682 {:method => :post, :path => '/issues/1/edit'}
675 )
683 )
676 end
684 end
677
685
678 def test_get_edit
686 def test_get_edit
679 @request.session[:user_id] = 2
687 @request.session[:user_id] = 2
680 get :edit, :id => 1
688 get :edit, :id => 1
681 assert_response :success
689 assert_response :success
682 assert_template 'edit'
690 assert_template 'edit'
683 assert_not_nil assigns(:issue)
691 assert_not_nil assigns(:issue)
684 assert_equal Issue.find(1), assigns(:issue)
692 assert_equal Issue.find(1), assigns(:issue)
685 end
693 end
686
694
687 def test_get_edit_with_params
695 def test_get_edit_with_params
688 @request.session[:user_id] = 2
696 @request.session[:user_id] = 2
689 get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 }
697 get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 }
690 assert_response :success
698 assert_response :success
691 assert_template 'edit'
699 assert_template 'edit'
692
700
693 issue = assigns(:issue)
701 issue = assigns(:issue)
694 assert_not_nil issue
702 assert_not_nil issue
695
703
696 assert_equal 5, issue.status_id
704 assert_equal 5, issue.status_id
697 assert_tag :select, :attributes => { :name => 'issue[status_id]' },
705 assert_tag :select, :attributes => { :name => 'issue[status_id]' },
698 :child => { :tag => 'option',
706 :child => { :tag => 'option',
699 :content => 'Closed',
707 :content => 'Closed',
700 :attributes => { :selected => 'selected' } }
708 :attributes => { :selected => 'selected' } }
701
709
702 assert_equal 7, issue.priority_id
710 assert_equal 7, issue.priority_id
703 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
711 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
704 :child => { :tag => 'option',
712 :child => { :tag => 'option',
705 :content => 'Urgent',
713 :content => 'Urgent',
706 :attributes => { :selected => 'selected' } }
714 :attributes => { :selected => 'selected' } }
707 end
715 end
708
716
709 def test_update_edit_form
717 def test_update_edit_form
710 @request.session[:user_id] = 2
718 @request.session[:user_id] = 2
711 xhr :post, :update_form, :project_id => 1,
719 xhr :post, :update_form, :project_id => 1,
712 :id => 1,
720 :id => 1,
713 :issue => {:tracker_id => 2,
721 :issue => {:tracker_id => 2,
714 :subject => 'This is the test_new issue',
722 :subject => 'This is the test_new issue',
715 :description => 'This is the description',
723 :description => 'This is the description',
716 :priority_id => 5}
724 :priority_id => 5}
717 assert_response :success
725 assert_response :success
718 assert_template 'attributes'
726 assert_template 'attributes'
719
727
720 issue = assigns(:issue)
728 issue = assigns(:issue)
721 assert_kind_of Issue, issue
729 assert_kind_of Issue, issue
722 assert_equal 1, issue.id
730 assert_equal 1, issue.id
723 assert_equal 1, issue.project_id
731 assert_equal 1, issue.project_id
724 assert_equal 2, issue.tracker_id
732 assert_equal 2, issue.tracker_id
725 assert_equal 'This is the test_new issue', issue.subject
733 assert_equal 'This is the test_new issue', issue.subject
726 end
734 end
727
735
728 def test_reply_routing
736 def test_reply_routing
729 assert_routing(
737 assert_routing(
730 {:method => :post, :path => '/issues/1/quoted'},
738 {:method => :post, :path => '/issues/1/quoted'},
731 :controller => 'issues', :action => 'reply', :id => '1'
739 :controller => 'issues', :action => 'reply', :id => '1'
732 )
740 )
733 end
741 end
734
742
735 def test_reply_to_issue
743 def test_reply_to_issue
736 @request.session[:user_id] = 2
744 @request.session[:user_id] = 2
737 get :reply, :id => 1
745 get :reply, :id => 1
738 assert_response :success
746 assert_response :success
739 assert_select_rjs :show, "update"
747 assert_select_rjs :show, "update"
740 end
748 end
741
749
742 def test_reply_to_note
750 def test_reply_to_note
743 @request.session[:user_id] = 2
751 @request.session[:user_id] = 2
744 get :reply, :id => 1, :journal_id => 2
752 get :reply, :id => 1, :journal_id => 2
745 assert_response :success
753 assert_response :success
746 assert_select_rjs :show, "update"
754 assert_select_rjs :show, "update"
747 end
755 end
748
756
749 def test_post_edit_without_custom_fields_param
757 def test_post_edit_without_custom_fields_param
750 @request.session[:user_id] = 2
758 @request.session[:user_id] = 2
751 ActionMailer::Base.deliveries.clear
759 ActionMailer::Base.deliveries.clear
752
760
753 issue = Issue.find(1)
761 issue = Issue.find(1)
754 assert_equal '125', issue.custom_value_for(2).value
762 assert_equal '125', issue.custom_value_for(2).value
755 old_subject = issue.subject
763 old_subject = issue.subject
756 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
764 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
757
765
758 assert_difference('Journal.count') do
766 assert_difference('Journal.count') do
759 assert_difference('JournalDetail.count', 2) do
767 assert_difference('JournalDetail.count', 2) do
760 post :edit, :id => 1, :issue => {:subject => new_subject,
768 post :edit, :id => 1, :issue => {:subject => new_subject,
761 :priority_id => '6',
769 :priority_id => '6',
762 :category_id => '1' # no change
770 :category_id => '1' # no change
763 }
771 }
764 end
772 end
765 end
773 end
766 assert_redirected_to :action => 'show', :id => '1'
774 assert_redirected_to :action => 'show', :id => '1'
767 issue.reload
775 issue.reload
768 assert_equal new_subject, issue.subject
776 assert_equal new_subject, issue.subject
769 # Make sure custom fields were not cleared
777 # Make sure custom fields were not cleared
770 assert_equal '125', issue.custom_value_for(2).value
778 assert_equal '125', issue.custom_value_for(2).value
771
779
772 mail = ActionMailer::Base.deliveries.last
780 mail = ActionMailer::Base.deliveries.last
773 assert_kind_of TMail::Mail, mail
781 assert_kind_of TMail::Mail, mail
774 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
782 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
775 assert mail.body.include?("Subject changed from #{old_subject} to #{new_subject}")
783 assert mail.body.include?("Subject changed from #{old_subject} to #{new_subject}")
776 end
784 end
777
785
778 def test_post_edit_with_custom_field_change
786 def test_post_edit_with_custom_field_change
779 @request.session[:user_id] = 2
787 @request.session[:user_id] = 2
780 issue = Issue.find(1)
788 issue = Issue.find(1)
781 assert_equal '125', issue.custom_value_for(2).value
789 assert_equal '125', issue.custom_value_for(2).value
782
790
783 assert_difference('Journal.count') do
791 assert_difference('Journal.count') do
784 assert_difference('JournalDetail.count', 3) do
792 assert_difference('JournalDetail.count', 3) do
785 post :edit, :id => 1, :issue => {:subject => 'Custom field change',
793 post :edit, :id => 1, :issue => {:subject => 'Custom field change',
786 :priority_id => '6',
794 :priority_id => '6',
787 :category_id => '1', # no change
795 :category_id => '1', # no change
788 :custom_field_values => { '2' => 'New custom value' }
796 :custom_field_values => { '2' => 'New custom value' }
789 }
797 }
790 end
798 end
791 end
799 end
792 assert_redirected_to :action => 'show', :id => '1'
800 assert_redirected_to :action => 'show', :id => '1'
793 issue.reload
801 issue.reload
794 assert_equal 'New custom value', issue.custom_value_for(2).value
802 assert_equal 'New custom value', issue.custom_value_for(2).value
795
803
796 mail = ActionMailer::Base.deliveries.last
804 mail = ActionMailer::Base.deliveries.last
797 assert_kind_of TMail::Mail, mail
805 assert_kind_of TMail::Mail, mail
798 assert mail.body.include?("Searchable field changed from 125 to New custom value")
806 assert mail.body.include?("Searchable field changed from 125 to New custom value")
799 end
807 end
800
808
801 def test_post_edit_with_status_and_assignee_change
809 def test_post_edit_with_status_and_assignee_change
802 issue = Issue.find(1)
810 issue = Issue.find(1)
803 assert_equal 1, issue.status_id
811 assert_equal 1, issue.status_id
804 @request.session[:user_id] = 2
812 @request.session[:user_id] = 2
805 assert_difference('TimeEntry.count', 0) do
813 assert_difference('TimeEntry.count', 0) do
806 post :edit,
814 post :edit,
807 :id => 1,
815 :id => 1,
808 :issue => { :status_id => 2, :assigned_to_id => 3 },
816 :issue => { :status_id => 2, :assigned_to_id => 3 },
809 :notes => 'Assigned to dlopper',
817 :notes => 'Assigned to dlopper',
810 :time_entry => { :hours => '', :comments => '', :activity_id => TimeEntryActivity.first }
818 :time_entry => { :hours => '', :comments => '', :activity_id => TimeEntryActivity.first }
811 end
819 end
812 assert_redirected_to :action => 'show', :id => '1'
820 assert_redirected_to :action => 'show', :id => '1'
813 issue.reload
821 issue.reload
814 assert_equal 2, issue.status_id
822 assert_equal 2, issue.status_id
815 j = Journal.find(:first, :order => 'id DESC')
823 j = Journal.find(:first, :order => 'id DESC')
816 assert_equal 'Assigned to dlopper', j.notes
824 assert_equal 'Assigned to dlopper', j.notes
817 assert_equal 2, j.details.size
825 assert_equal 2, j.details.size
818
826
819 mail = ActionMailer::Base.deliveries.last
827 mail = ActionMailer::Base.deliveries.last
820 assert mail.body.include?("Status changed from New to Assigned")
828 assert mail.body.include?("Status changed from New to Assigned")
821 # subject should contain the new status
829 # subject should contain the new status
822 assert mail.subject.include?("(#{ IssueStatus.find(2).name })")
830 assert mail.subject.include?("(#{ IssueStatus.find(2).name })")
823 end
831 end
824
832
825 def test_post_edit_with_note_only
833 def test_post_edit_with_note_only
826 notes = 'Note added by IssuesControllerTest#test_update_with_note_only'
834 notes = 'Note added by IssuesControllerTest#test_update_with_note_only'
827 # anonymous user
835 # anonymous user
828 post :edit,
836 post :edit,
829 :id => 1,
837 :id => 1,
830 :notes => notes
838 :notes => notes
831 assert_redirected_to :action => 'show', :id => '1'
839 assert_redirected_to :action => 'show', :id => '1'
832 j = Journal.find(:first, :order => 'id DESC')
840 j = Journal.find(:first, :order => 'id DESC')
833 assert_equal notes, j.notes
841 assert_equal notes, j.notes
834 assert_equal 0, j.details.size
842 assert_equal 0, j.details.size
835 assert_equal User.anonymous, j.user
843 assert_equal User.anonymous, j.user
836
844
837 mail = ActionMailer::Base.deliveries.last
845 mail = ActionMailer::Base.deliveries.last
838 assert mail.body.include?(notes)
846 assert mail.body.include?(notes)
839 end
847 end
840
848
841 def test_post_edit_with_note_and_spent_time
849 def test_post_edit_with_note_and_spent_time
842 @request.session[:user_id] = 2
850 @request.session[:user_id] = 2
843 spent_hours_before = Issue.find(1).spent_hours
851 spent_hours_before = Issue.find(1).spent_hours
844 assert_difference('TimeEntry.count') do
852 assert_difference('TimeEntry.count') do
845 post :edit,
853 post :edit,
846 :id => 1,
854 :id => 1,
847 :notes => '2.5 hours added',
855 :notes => '2.5 hours added',
848 :time_entry => { :hours => '2.5', :comments => '', :activity_id => TimeEntryActivity.first }
856 :time_entry => { :hours => '2.5', :comments => '', :activity_id => TimeEntryActivity.first }
849 end
857 end
850 assert_redirected_to :action => 'show', :id => '1'
858 assert_redirected_to :action => 'show', :id => '1'
851
859
852 issue = Issue.find(1)
860 issue = Issue.find(1)
853
861
854 j = Journal.find(:first, :order => 'id DESC')
862 j = Journal.find(:first, :order => 'id DESC')
855 assert_equal '2.5 hours added', j.notes
863 assert_equal '2.5 hours added', j.notes
856 assert_equal 0, j.details.size
864 assert_equal 0, j.details.size
857
865
858 t = issue.time_entries.find(:first, :order => 'id DESC')
866 t = issue.time_entries.find(:first, :order => 'id DESC')
859 assert_not_nil t
867 assert_not_nil t
860 assert_equal 2.5, t.hours
868 assert_equal 2.5, t.hours
861 assert_equal spent_hours_before + 2.5, issue.spent_hours
869 assert_equal spent_hours_before + 2.5, issue.spent_hours
862 end
870 end
863
871
864 def test_post_edit_with_attachment_only
872 def test_post_edit_with_attachment_only
865 set_tmp_attachments_directory
873 set_tmp_attachments_directory
866
874
867 # Delete all fixtured journals, a race condition can occur causing the wrong
875 # Delete all fixtured journals, a race condition can occur causing the wrong
868 # journal to get fetched in the next find.
876 # journal to get fetched in the next find.
869 Journal.delete_all
877 Journal.delete_all
870
878
871 # anonymous user
879 # anonymous user
872 post :edit,
880 post :edit,
873 :id => 1,
881 :id => 1,
874 :notes => '',
882 :notes => '',
875 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
883 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
876 assert_redirected_to :action => 'show', :id => '1'
884 assert_redirected_to :action => 'show', :id => '1'
877 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
885 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
878 assert j.notes.blank?
886 assert j.notes.blank?
879 assert_equal 1, j.details.size
887 assert_equal 1, j.details.size
880 assert_equal 'testfile.txt', j.details.first.value
888 assert_equal 'testfile.txt', j.details.first.value
881 assert_equal User.anonymous, j.user
889 assert_equal User.anonymous, j.user
882
890
883 mail = ActionMailer::Base.deliveries.last
891 mail = ActionMailer::Base.deliveries.last
884 assert mail.body.include?('testfile.txt')
892 assert mail.body.include?('testfile.txt')
885 end
893 end
886
894
887 def test_post_edit_with_no_change
895 def test_post_edit_with_no_change
888 issue = Issue.find(1)
896 issue = Issue.find(1)
889 issue.journals.clear
897 issue.journals.clear
890 ActionMailer::Base.deliveries.clear
898 ActionMailer::Base.deliveries.clear
891
899
892 post :edit,
900 post :edit,
893 :id => 1,
901 :id => 1,
894 :notes => ''
902 :notes => ''
895 assert_redirected_to :action => 'show', :id => '1'
903 assert_redirected_to :action => 'show', :id => '1'
896
904
897 issue.reload
905 issue.reload
898 assert issue.journals.empty?
906 assert issue.journals.empty?
899 # No email should be sent
907 # No email should be sent
900 assert ActionMailer::Base.deliveries.empty?
908 assert ActionMailer::Base.deliveries.empty?
901 end
909 end
902
910
903 def test_post_edit_should_send_a_notification
911 def test_post_edit_should_send_a_notification
904 @request.session[:user_id] = 2
912 @request.session[:user_id] = 2
905 ActionMailer::Base.deliveries.clear
913 ActionMailer::Base.deliveries.clear
906 issue = Issue.find(1)
914 issue = Issue.find(1)
907 old_subject = issue.subject
915 old_subject = issue.subject
908 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
916 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
909
917
910 post :edit, :id => 1, :issue => {:subject => new_subject,
918 post :edit, :id => 1, :issue => {:subject => new_subject,
911 :priority_id => '6',
919 :priority_id => '6',
912 :category_id => '1' # no change
920 :category_id => '1' # no change
913 }
921 }
914 assert_equal 1, ActionMailer::Base.deliveries.size
922 assert_equal 1, ActionMailer::Base.deliveries.size
915 end
923 end
916
924
917 def test_post_edit_with_invalid_spent_time
925 def test_post_edit_with_invalid_spent_time
918 @request.session[:user_id] = 2
926 @request.session[:user_id] = 2
919 notes = 'Note added by IssuesControllerTest#test_post_edit_with_invalid_spent_time'
927 notes = 'Note added by IssuesControllerTest#test_post_edit_with_invalid_spent_time'
920
928
921 assert_no_difference('Journal.count') do
929 assert_no_difference('Journal.count') do
922 post :edit,
930 post :edit,
923 :id => 1,
931 :id => 1,
924 :notes => notes,
932 :notes => notes,
925 :time_entry => {"comments"=>"", "activity_id"=>"", "hours"=>"2z"}
933 :time_entry => {"comments"=>"", "activity_id"=>"", "hours"=>"2z"}
926 end
934 end
927 assert_response :success
935 assert_response :success
928 assert_template 'edit'
936 assert_template 'edit'
929
937
930 assert_tag :textarea, :attributes => { :name => 'notes' },
938 assert_tag :textarea, :attributes => { :name => 'notes' },
931 :content => notes
939 :content => notes
932 assert_tag :input, :attributes => { :name => 'time_entry[hours]', :value => "2z" }
940 assert_tag :input, :attributes => { :name => 'time_entry[hours]', :value => "2z" }
933 end
941 end
934
942
935 def test_post_edit_should_allow_fixed_version_to_be_set_to_a_subproject
943 def test_post_edit_should_allow_fixed_version_to_be_set_to_a_subproject
936 issue = Issue.find(2)
944 issue = Issue.find(2)
937 @request.session[:user_id] = 2
945 @request.session[:user_id] = 2
938
946
939 post :edit,
947 post :edit,
940 :id => issue.id,
948 :id => issue.id,
941 :issue => {
949 :issue => {
942 :fixed_version_id => 4
950 :fixed_version_id => 4
943 }
951 }
944
952
945 assert_response :redirect
953 assert_response :redirect
946 issue.reload
954 issue.reload
947 assert_equal 4, issue.fixed_version_id
955 assert_equal 4, issue.fixed_version_id
948 assert_not_equal issue.project_id, issue.fixed_version.project_id
956 assert_not_equal issue.project_id, issue.fixed_version.project_id
949 end
957 end
950
958
951 def test_post_edit_should_redirect_back_using_the_back_url_parameter
959 def test_post_edit_should_redirect_back_using_the_back_url_parameter
952 issue = Issue.find(2)
960 issue = Issue.find(2)
953 @request.session[:user_id] = 2
961 @request.session[:user_id] = 2
954
962
955 post :edit,
963 post :edit,
956 :id => issue.id,
964 :id => issue.id,
957 :issue => {
965 :issue => {
958 :fixed_version_id => 4
966 :fixed_version_id => 4
959 },
967 },
960 :back_url => '/issues'
968 :back_url => '/issues'
961
969
962 assert_response :redirect
970 assert_response :redirect
963 assert_redirected_to '/issues'
971 assert_redirected_to '/issues'
964 end
972 end
965
973
966 def test_post_edit_should_not_redirect_back_using_the_back_url_parameter_off_the_host
974 def test_post_edit_should_not_redirect_back_using_the_back_url_parameter_off_the_host
967 issue = Issue.find(2)
975 issue = Issue.find(2)
968 @request.session[:user_id] = 2
976 @request.session[:user_id] = 2
969
977
970 post :edit,
978 post :edit,
971 :id => issue.id,
979 :id => issue.id,
972 :issue => {
980 :issue => {
973 :fixed_version_id => 4
981 :fixed_version_id => 4
974 },
982 },
975 :back_url => 'http://google.com'
983 :back_url => 'http://google.com'
976
984
977 assert_response :redirect
985 assert_response :redirect
978 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue.id
986 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue.id
979 end
987 end
980
988
981 def test_get_bulk_edit
989 def test_get_bulk_edit
982 @request.session[:user_id] = 2
990 @request.session[:user_id] = 2
983 get :bulk_edit, :ids => [1, 2]
991 get :bulk_edit, :ids => [1, 2]
984 assert_response :success
992 assert_response :success
985 assert_template 'bulk_edit'
993 assert_template 'bulk_edit'
986
994
987 # Project specific custom field, date type
995 # Project specific custom field, date type
988 field = CustomField.find(9)
996 field = CustomField.find(9)
989 assert !field.is_for_all?
997 assert !field.is_for_all?
990 assert_equal 'date', field.field_format
998 assert_equal 'date', field.field_format
991 assert_tag :input, :attributes => {:name => 'custom_field_values[9]'}
999 assert_tag :input, :attributes => {:name => 'custom_field_values[9]'}
992
1000
993 # System wide custom field
1001 # System wide custom field
994 assert CustomField.find(1).is_for_all?
1002 assert CustomField.find(1).is_for_all?
995 assert_tag :select, :attributes => {:name => 'custom_field_values[1]'}
1003 assert_tag :select, :attributes => {:name => 'custom_field_values[1]'}
996 end
1004 end
997
1005
998 def test_bulk_edit
1006 def test_bulk_edit
999 @request.session[:user_id] = 2
1007 @request.session[:user_id] = 2
1000 # update issues priority
1008 # update issues priority
1001 post :bulk_edit, :ids => [1, 2], :priority_id => 7,
1009 post :bulk_edit, :ids => [1, 2], :priority_id => 7,
1002 :assigned_to_id => '',
1010 :assigned_to_id => '',
1003 :custom_field_values => {'2' => ''},
1011 :custom_field_values => {'2' => ''},
1004 :notes => 'Bulk editing'
1012 :notes => 'Bulk editing'
1005 assert_response 302
1013 assert_response 302
1006 # check that the issues were updated
1014 # check that the issues were updated
1007 assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id}
1015 assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id}
1008
1016
1009 issue = Issue.find(1)
1017 issue = Issue.find(1)
1010 journal = issue.journals.find(:first, :order => 'created_on DESC')
1018 journal = issue.journals.find(:first, :order => 'created_on DESC')
1011 assert_equal '125', issue.custom_value_for(2).value
1019 assert_equal '125', issue.custom_value_for(2).value
1012 assert_equal 'Bulk editing', journal.notes
1020 assert_equal 'Bulk editing', journal.notes
1013 assert_equal 1, journal.details.size
1021 assert_equal 1, journal.details.size
1014 end
1022 end
1015
1023
1016 def test_bullk_edit_should_send_a_notification
1024 def test_bullk_edit_should_send_a_notification
1017 @request.session[:user_id] = 2
1025 @request.session[:user_id] = 2
1018 ActionMailer::Base.deliveries.clear
1026 ActionMailer::Base.deliveries.clear
1019 post(:bulk_edit,
1027 post(:bulk_edit,
1020 {
1028 {
1021 :ids => [1, 2],
1029 :ids => [1, 2],
1022 :priority_id => 7,
1030 :priority_id => 7,
1023 :assigned_to_id => '',
1031 :assigned_to_id => '',
1024 :custom_field_values => {'2' => ''},
1032 :custom_field_values => {'2' => ''},
1025 :notes => 'Bulk editing'
1033 :notes => 'Bulk editing'
1026 })
1034 })
1027
1035
1028 assert_response 302
1036 assert_response 302
1029 assert_equal 2, ActionMailer::Base.deliveries.size
1037 assert_equal 2, ActionMailer::Base.deliveries.size
1030 end
1038 end
1031
1039
1032 def test_bulk_edit_status
1040 def test_bulk_edit_status
1033 @request.session[:user_id] = 2
1041 @request.session[:user_id] = 2
1034 # update issues priority
1042 # update issues priority
1035 post :bulk_edit, :ids => [1, 2], :priority_id => '',
1043 post :bulk_edit, :ids => [1, 2], :priority_id => '',
1036 :assigned_to_id => '',
1044 :assigned_to_id => '',
1037 :status_id => '5',
1045 :status_id => '5',
1038 :notes => 'Bulk editing status'
1046 :notes => 'Bulk editing status'
1039 assert_response 302
1047 assert_response 302
1040 issue = Issue.find(1)
1048 issue = Issue.find(1)
1041 assert issue.closed?
1049 assert issue.closed?
1042 end
1050 end
1043
1051
1044 def test_bulk_edit_custom_field
1052 def test_bulk_edit_custom_field
1045 @request.session[:user_id] = 2
1053 @request.session[:user_id] = 2
1046 # update issues priority
1054 # update issues priority
1047 post :bulk_edit, :ids => [1, 2], :priority_id => '',
1055 post :bulk_edit, :ids => [1, 2], :priority_id => '',
1048 :assigned_to_id => '',
1056 :assigned_to_id => '',
1049 :custom_field_values => {'2' => '777'},
1057 :custom_field_values => {'2' => '777'},
1050 :notes => 'Bulk editing custom field'
1058 :notes => 'Bulk editing custom field'
1051 assert_response 302
1059 assert_response 302
1052
1060
1053 issue = Issue.find(1)
1061 issue = Issue.find(1)
1054 journal = issue.journals.find(:first, :order => 'created_on DESC')
1062 journal = issue.journals.find(:first, :order => 'created_on DESC')
1055 assert_equal '777', issue.custom_value_for(2).value
1063 assert_equal '777', issue.custom_value_for(2).value
1056 assert_equal 1, journal.details.size
1064 assert_equal 1, journal.details.size
1057 assert_equal '125', journal.details.first.old_value
1065 assert_equal '125', journal.details.first.old_value
1058 assert_equal '777', journal.details.first.value
1066 assert_equal '777', journal.details.first.value
1059 end
1067 end
1060
1068
1061 def test_bulk_unassign
1069 def test_bulk_unassign
1062 assert_not_nil Issue.find(2).assigned_to
1070 assert_not_nil Issue.find(2).assigned_to
1063 @request.session[:user_id] = 2
1071 @request.session[:user_id] = 2
1064 # unassign issues
1072 # unassign issues
1065 post :bulk_edit, :ids => [1, 2], :notes => 'Bulk unassigning', :assigned_to_id => 'none'
1073 post :bulk_edit, :ids => [1, 2], :notes => 'Bulk unassigning', :assigned_to_id => 'none'
1066 assert_response 302
1074 assert_response 302
1067 # check that the issues were updated
1075 # check that the issues were updated
1068 assert_nil Issue.find(2).assigned_to
1076 assert_nil Issue.find(2).assigned_to
1069 end
1077 end
1070
1078
1071 def test_post_bulk_edit_should_allow_fixed_version_to_be_set_to_a_subproject
1079 def test_post_bulk_edit_should_allow_fixed_version_to_be_set_to_a_subproject
1072 @request.session[:user_id] = 2
1080 @request.session[:user_id] = 2
1073
1081
1074 post :bulk_edit,
1082 post :bulk_edit,
1075 :ids => [1,2],
1083 :ids => [1,2],
1076 :fixed_version_id => 4
1084 :fixed_version_id => 4
1077
1085
1078 assert_response :redirect
1086 assert_response :redirect
1079 issues = Issue.find([1,2])
1087 issues = Issue.find([1,2])
1080 issues.each do |issue|
1088 issues.each do |issue|
1081 assert_equal 4, issue.fixed_version_id
1089 assert_equal 4, issue.fixed_version_id
1082 assert_not_equal issue.project_id, issue.fixed_version.project_id
1090 assert_not_equal issue.project_id, issue.fixed_version.project_id
1083 end
1091 end
1084 end
1092 end
1085
1093
1086 def test_post_bulk_edit_should_redirect_back_using_the_back_url_parameter
1094 def test_post_bulk_edit_should_redirect_back_using_the_back_url_parameter
1087 @request.session[:user_id] = 2
1095 @request.session[:user_id] = 2
1088 post :bulk_edit, :ids => [1,2], :back_url => '/issues'
1096 post :bulk_edit, :ids => [1,2], :back_url => '/issues'
1089
1097
1090 assert_response :redirect
1098 assert_response :redirect
1091 assert_redirected_to '/issues'
1099 assert_redirected_to '/issues'
1092 end
1100 end
1093
1101
1094 def test_post_bulk_edit_should_not_redirect_back_using_the_back_url_parameter_off_the_host
1102 def test_post_bulk_edit_should_not_redirect_back_using_the_back_url_parameter_off_the_host
1095 @request.session[:user_id] = 2
1103 @request.session[:user_id] = 2
1096 post :bulk_edit, :ids => [1,2], :back_url => 'http://google.com'
1104 post :bulk_edit, :ids => [1,2], :back_url => 'http://google.com'
1097
1105
1098 assert_response :redirect
1106 assert_response :redirect
1099 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => Project.find(1).identifier
1107 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => Project.find(1).identifier
1100 end
1108 end
1101
1109
1102 def test_move_routing
1110 def test_move_routing
1103 assert_routing(
1111 assert_routing(
1104 {:method => :get, :path => '/issues/1/move'},
1112 {:method => :get, :path => '/issues/1/move'},
1105 :controller => 'issues', :action => 'move', :id => '1'
1113 :controller => 'issues', :action => 'move', :id => '1'
1106 )
1114 )
1107 assert_recognizes(
1115 assert_recognizes(
1108 {:controller => 'issues', :action => 'move', :id => '1'},
1116 {:controller => 'issues', :action => 'move', :id => '1'},
1109 {:method => :post, :path => '/issues/1/move'}
1117 {:method => :post, :path => '/issues/1/move'}
1110 )
1118 )
1111 end
1119 end
1112
1120
1113 def test_move_one_issue_to_another_project
1121 def test_move_one_issue_to_another_project
1114 @request.session[:user_id] = 2
1122 @request.session[:user_id] = 2
1115 post :move, :id => 1, :new_project_id => 2, :tracker_id => '', :assigned_to_id => '', :status_id => '', :start_date => '', :due_date => ''
1123 post :move, :id => 1, :new_project_id => 2, :tracker_id => '', :assigned_to_id => '', :status_id => '', :start_date => '', :due_date => ''
1116 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1124 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1117 assert_equal 2, Issue.find(1).project_id
1125 assert_equal 2, Issue.find(1).project_id
1118 end
1126 end
1119
1127
1120 def test_move_one_issue_to_another_project_should_follow_when_needed
1128 def test_move_one_issue_to_another_project_should_follow_when_needed
1121 @request.session[:user_id] = 2
1129 @request.session[:user_id] = 2
1122 post :move, :id => 1, :new_project_id => 2, :follow => '1'
1130 post :move, :id => 1, :new_project_id => 2, :follow => '1'
1123 assert_redirected_to '/issues/1'
1131 assert_redirected_to '/issues/1'
1124 end
1132 end
1125
1133
1126 def test_bulk_move_to_another_project
1134 def test_bulk_move_to_another_project
1127 @request.session[:user_id] = 2
1135 @request.session[:user_id] = 2
1128 post :move, :ids => [1, 2], :new_project_id => 2
1136 post :move, :ids => [1, 2], :new_project_id => 2
1129 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1137 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1130 # Issues moved to project 2
1138 # Issues moved to project 2
1131 assert_equal 2, Issue.find(1).project_id
1139 assert_equal 2, Issue.find(1).project_id
1132 assert_equal 2, Issue.find(2).project_id
1140 assert_equal 2, Issue.find(2).project_id
1133 # No tracker change
1141 # No tracker change
1134 assert_equal 1, Issue.find(1).tracker_id
1142 assert_equal 1, Issue.find(1).tracker_id
1135 assert_equal 2, Issue.find(2).tracker_id
1143 assert_equal 2, Issue.find(2).tracker_id
1136 end
1144 end
1137
1145
1138 def test_bulk_move_to_another_tracker
1146 def test_bulk_move_to_another_tracker
1139 @request.session[:user_id] = 2
1147 @request.session[:user_id] = 2
1140 post :move, :ids => [1, 2], :new_tracker_id => 2
1148 post :move, :ids => [1, 2], :new_tracker_id => 2
1141 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1149 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1142 assert_equal 2, Issue.find(1).tracker_id
1150 assert_equal 2, Issue.find(1).tracker_id
1143 assert_equal 2, Issue.find(2).tracker_id
1151 assert_equal 2, Issue.find(2).tracker_id
1144 end
1152 end
1145
1153
1146 def test_bulk_copy_to_another_project
1154 def test_bulk_copy_to_another_project
1147 @request.session[:user_id] = 2
1155 @request.session[:user_id] = 2
1148 assert_difference 'Issue.count', 2 do
1156 assert_difference 'Issue.count', 2 do
1149 assert_no_difference 'Project.find(1).issues.count' do
1157 assert_no_difference 'Project.find(1).issues.count' do
1150 post :move, :ids => [1, 2], :new_project_id => 2, :copy_options => {:copy => '1'}
1158 post :move, :ids => [1, 2], :new_project_id => 2, :copy_options => {:copy => '1'}
1151 end
1159 end
1152 end
1160 end
1153 assert_redirected_to 'projects/ecookbook/issues'
1161 assert_redirected_to 'projects/ecookbook/issues'
1154 end
1162 end
1155
1163
1156 context "#move via bulk copy" do
1164 context "#move via bulk copy" do
1157 should "allow not changing the issue's attributes" do
1165 should "allow not changing the issue's attributes" do
1158 @request.session[:user_id] = 2
1166 @request.session[:user_id] = 2
1159 issue_before_move = Issue.find(1)
1167 issue_before_move = Issue.find(1)
1160 assert_difference 'Issue.count', 1 do
1168 assert_difference 'Issue.count', 1 do
1161 assert_no_difference 'Project.find(1).issues.count' do
1169 assert_no_difference 'Project.find(1).issues.count' do
1162 post :move, :ids => [1], :new_project_id => 2, :copy_options => {:copy => '1'}, :new_tracker_id => '', :assigned_to_id => '', :status_id => '', :start_date => '', :due_date => ''
1170 post :move, :ids => [1], :new_project_id => 2, :copy_options => {:copy => '1'}, :new_tracker_id => '', :assigned_to_id => '', :status_id => '', :start_date => '', :due_date => ''
1163 end
1171 end
1164 end
1172 end
1165 issue_after_move = Issue.first(:order => 'id desc', :conditions => {:project_id => 2})
1173 issue_after_move = Issue.first(:order => 'id desc', :conditions => {:project_id => 2})
1166 assert_equal issue_before_move.tracker_id, issue_after_move.tracker_id
1174 assert_equal issue_before_move.tracker_id, issue_after_move.tracker_id
1167 assert_equal issue_before_move.status_id, issue_after_move.status_id
1175 assert_equal issue_before_move.status_id, issue_after_move.status_id
1168 assert_equal issue_before_move.assigned_to_id, issue_after_move.assigned_to_id
1176 assert_equal issue_before_move.assigned_to_id, issue_after_move.assigned_to_id
1169 end
1177 end
1170
1178
1171 should "allow changing the issue's attributes" do
1179 should "allow changing the issue's attributes" do
1172 @request.session[:user_id] = 2
1180 @request.session[:user_id] = 2
1173 assert_difference 'Issue.count', 2 do
1181 assert_difference 'Issue.count', 2 do
1174 assert_no_difference 'Project.find(1).issues.count' do
1182 assert_no_difference 'Project.find(1).issues.count' do
1175 post :move, :ids => [1, 2], :new_project_id => 2, :copy_options => {:copy => '1'}, :new_tracker_id => '', :assigned_to_id => 4, :status_id => 3, :start_date => '2009-12-01', :due_date => '2009-12-31'
1183 post :move, :ids => [1, 2], :new_project_id => 2, :copy_options => {:copy => '1'}, :new_tracker_id => '', :assigned_to_id => 4, :status_id => 3, :start_date => '2009-12-01', :due_date => '2009-12-31'
1176 end
1184 end
1177 end
1185 end
1178
1186
1179 copied_issues = Issue.all(:limit => 2, :order => 'id desc', :conditions => {:project_id => 2})
1187 copied_issues = Issue.all(:limit => 2, :order => 'id desc', :conditions => {:project_id => 2})
1180 assert_equal 2, copied_issues.size
1188 assert_equal 2, copied_issues.size
1181 copied_issues.each do |issue|
1189 copied_issues.each do |issue|
1182 assert_equal 2, issue.project_id, "Project is incorrect"
1190 assert_equal 2, issue.project_id, "Project is incorrect"
1183 assert_equal 4, issue.assigned_to_id, "Assigned to is incorrect"
1191 assert_equal 4, issue.assigned_to_id, "Assigned to is incorrect"
1184 assert_equal 3, issue.status_id, "Status is incorrect"
1192 assert_equal 3, issue.status_id, "Status is incorrect"
1185 assert_equal '2009-12-01', issue.start_date.to_s, "Start date is incorrect"
1193 assert_equal '2009-12-01', issue.start_date.to_s, "Start date is incorrect"
1186 assert_equal '2009-12-31', issue.due_date.to_s, "Due date is incorrect"
1194 assert_equal '2009-12-31', issue.due_date.to_s, "Due date is incorrect"
1187 end
1195 end
1188 end
1196 end
1189 end
1197 end
1190
1198
1191 def test_copy_to_another_project_should_follow_when_needed
1199 def test_copy_to_another_project_should_follow_when_needed
1192 @request.session[:user_id] = 2
1200 @request.session[:user_id] = 2
1193 post :move, :ids => [1], :new_project_id => 2, :copy_options => {:copy => '1'}, :follow => '1'
1201 post :move, :ids => [1], :new_project_id => 2, :copy_options => {:copy => '1'}, :follow => '1'
1194 issue = Issue.first(:order => 'id DESC')
1202 issue = Issue.first(:order => 'id DESC')
1195 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
1203 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
1196 end
1204 end
1197
1205
1198 def test_context_menu_one_issue
1206 def test_context_menu_one_issue
1199 @request.session[:user_id] = 2
1207 @request.session[:user_id] = 2
1200 get :context_menu, :ids => [1]
1208 get :context_menu, :ids => [1]
1201 assert_response :success
1209 assert_response :success
1202 assert_template 'context_menu'
1210 assert_template 'context_menu'
1203 assert_tag :tag => 'a', :content => 'Edit',
1211 assert_tag :tag => 'a', :content => 'Edit',
1204 :attributes => { :href => '/issues/1/edit',
1212 :attributes => { :href => '/issues/1/edit',
1205 :class => 'icon-edit' }
1213 :class => 'icon-edit' }
1206 assert_tag :tag => 'a', :content => 'Closed',
1214 assert_tag :tag => 'a', :content => 'Closed',
1207 :attributes => { :href => '/issues/1/edit?issue%5Bstatus_id%5D=5',
1215 :attributes => { :href => '/issues/1/edit?issue%5Bstatus_id%5D=5',
1208 :class => '' }
1216 :class => '' }
1209 assert_tag :tag => 'a', :content => 'Immediate',
1217 assert_tag :tag => 'a', :content => 'Immediate',
1210 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;priority_id=8',
1218 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;priority_id=8',
1211 :class => '' }
1219 :class => '' }
1212 # Versions
1220 # Versions
1213 assert_tag :tag => 'a', :content => '2.0',
1221 assert_tag :tag => 'a', :content => '2.0',
1214 :attributes => { :href => '/issues/bulk_edit?fixed_version_id=3&amp;ids%5B%5D=1',
1222 :attributes => { :href => '/issues/bulk_edit?fixed_version_id=3&amp;ids%5B%5D=1',
1215 :class => '' }
1223 :class => '' }
1216 assert_tag :tag => 'a', :content => 'eCookbook Subproject 1 - 2.0',
1224 assert_tag :tag => 'a', :content => 'eCookbook Subproject 1 - 2.0',
1217 :attributes => { :href => '/issues/bulk_edit?fixed_version_id=4&amp;ids%5B%5D=1',
1225 :attributes => { :href => '/issues/bulk_edit?fixed_version_id=4&amp;ids%5B%5D=1',
1218 :class => '' }
1226 :class => '' }
1219
1227
1220 assert_tag :tag => 'a', :content => 'Dave Lopper',
1228 assert_tag :tag => 'a', :content => 'Dave Lopper',
1221 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1',
1229 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1',
1222 :class => '' }
1230 :class => '' }
1223 assert_tag :tag => 'a', :content => 'Duplicate',
1231 assert_tag :tag => 'a', :content => 'Duplicate',
1224 :attributes => { :href => '/projects/ecookbook/issues/1/copy',
1232 :attributes => { :href => '/projects/ecookbook/issues/1/copy',
1225 :class => 'icon-duplicate' }
1233 :class => 'icon-duplicate' }
1226 assert_tag :tag => 'a', :content => 'Copy',
1234 assert_tag :tag => 'a', :content => 'Copy',
1227 :attributes => { :href => '/issues/move?copy_options%5Bcopy%5D=t&amp;ids%5B%5D=1',
1235 :attributes => { :href => '/issues/move?copy_options%5Bcopy%5D=t&amp;ids%5B%5D=1',
1228 :class => 'icon-copy' }
1236 :class => 'icon-copy' }
1229 assert_tag :tag => 'a', :content => 'Move',
1237 assert_tag :tag => 'a', :content => 'Move',
1230 :attributes => { :href => '/issues/move?ids%5B%5D=1',
1238 :attributes => { :href => '/issues/move?ids%5B%5D=1',
1231 :class => 'icon-move' }
1239 :class => 'icon-move' }
1232 assert_tag :tag => 'a', :content => 'Delete',
1240 assert_tag :tag => 'a', :content => 'Delete',
1233 :attributes => { :href => '/issues/destroy?ids%5B%5D=1',
1241 :attributes => { :href => '/issues/destroy?ids%5B%5D=1',
1234 :class => 'icon-del' }
1242 :class => 'icon-del' }
1235 end
1243 end
1236
1244
1237 def test_context_menu_one_issue_by_anonymous
1245 def test_context_menu_one_issue_by_anonymous
1238 get :context_menu, :ids => [1]
1246 get :context_menu, :ids => [1]
1239 assert_response :success
1247 assert_response :success
1240 assert_template 'context_menu'
1248 assert_template 'context_menu'
1241 assert_tag :tag => 'a', :content => 'Delete',
1249 assert_tag :tag => 'a', :content => 'Delete',
1242 :attributes => { :href => '#',
1250 :attributes => { :href => '#',
1243 :class => 'icon-del disabled' }
1251 :class => 'icon-del disabled' }
1244 end
1252 end
1245
1253
1246 def test_context_menu_multiple_issues_of_same_project
1254 def test_context_menu_multiple_issues_of_same_project
1247 @request.session[:user_id] = 2
1255 @request.session[:user_id] = 2
1248 get :context_menu, :ids => [1, 2]
1256 get :context_menu, :ids => [1, 2]
1249 assert_response :success
1257 assert_response :success
1250 assert_template 'context_menu'
1258 assert_template 'context_menu'
1251 assert_tag :tag => 'a', :content => 'Edit',
1259 assert_tag :tag => 'a', :content => 'Edit',
1252 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2',
1260 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2',
1253 :class => 'icon-edit' }
1261 :class => 'icon-edit' }
1254 assert_tag :tag => 'a', :content => 'Immediate',
1262 assert_tag :tag => 'a', :content => 'Immediate',
1255 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2&amp;priority_id=8',
1263 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2&amp;priority_id=8',
1256 :class => '' }
1264 :class => '' }
1257 assert_tag :tag => 'a', :content => 'Dave Lopper',
1265 assert_tag :tag => 'a', :content => 'Dave Lopper',
1258 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1&amp;ids%5B%5D=2',
1266 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1&amp;ids%5B%5D=2',
1259 :class => '' }
1267 :class => '' }
1260 assert_tag :tag => 'a', :content => 'Copy',
1268 assert_tag :tag => 'a', :content => 'Copy',
1261 :attributes => { :href => '/issues/move?copy_options%5Bcopy%5D=t&amp;ids%5B%5D=1&amp;ids%5B%5D=2',
1269 :attributes => { :href => '/issues/move?copy_options%5Bcopy%5D=t&amp;ids%5B%5D=1&amp;ids%5B%5D=2',
1262 :class => 'icon-copy' }
1270 :class => 'icon-copy' }
1263 assert_tag :tag => 'a', :content => 'Move',
1271 assert_tag :tag => 'a', :content => 'Move',
1264 :attributes => { :href => '/issues/move?ids%5B%5D=1&amp;ids%5B%5D=2',
1272 :attributes => { :href => '/issues/move?ids%5B%5D=1&amp;ids%5B%5D=2',
1265 :class => 'icon-move' }
1273 :class => 'icon-move' }
1266 assert_tag :tag => 'a', :content => 'Delete',
1274 assert_tag :tag => 'a', :content => 'Delete',
1267 :attributes => { :href => '/issues/destroy?ids%5B%5D=1&amp;ids%5B%5D=2',
1275 :attributes => { :href => '/issues/destroy?ids%5B%5D=1&amp;ids%5B%5D=2',
1268 :class => 'icon-del' }
1276 :class => 'icon-del' }
1269 end
1277 end
1270
1278
1271 def test_context_menu_multiple_issues_of_different_project
1279 def test_context_menu_multiple_issues_of_different_project
1272 @request.session[:user_id] = 2
1280 @request.session[:user_id] = 2
1273 get :context_menu, :ids => [1, 2, 4]
1281 get :context_menu, :ids => [1, 2, 4]
1274 assert_response :success
1282 assert_response :success
1275 assert_template 'context_menu'
1283 assert_template 'context_menu'
1276 assert_tag :tag => 'a', :content => 'Delete',
1284 assert_tag :tag => 'a', :content => 'Delete',
1277 :attributes => { :href => '#',
1285 :attributes => { :href => '#',
1278 :class => 'icon-del disabled' }
1286 :class => 'icon-del disabled' }
1279 end
1287 end
1280
1288
1281 def test_destroy_routing
1289 def test_destroy_routing
1282 assert_recognizes( #TODO: use DELETE on issue URI (need to change forms)
1290 assert_recognizes( #TODO: use DELETE on issue URI (need to change forms)
1283 {:controller => 'issues', :action => 'destroy', :id => '1'},
1291 {:controller => 'issues', :action => 'destroy', :id => '1'},
1284 {:method => :post, :path => '/issues/1/destroy'}
1292 {:method => :post, :path => '/issues/1/destroy'}
1285 )
1293 )
1286 end
1294 end
1287
1295
1288 def test_destroy_issue_with_no_time_entries
1296 def test_destroy_issue_with_no_time_entries
1289 assert_nil TimeEntry.find_by_issue_id(2)
1297 assert_nil TimeEntry.find_by_issue_id(2)
1290 @request.session[:user_id] = 2
1298 @request.session[:user_id] = 2
1291 post :destroy, :id => 2
1299 post :destroy, :id => 2
1292 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1300 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1293 assert_nil Issue.find_by_id(2)
1301 assert_nil Issue.find_by_id(2)
1294 end
1302 end
1295
1303
1296 def test_destroy_issues_with_time_entries
1304 def test_destroy_issues_with_time_entries
1297 @request.session[:user_id] = 2
1305 @request.session[:user_id] = 2
1298 post :destroy, :ids => [1, 3]
1306 post :destroy, :ids => [1, 3]
1299 assert_response :success
1307 assert_response :success
1300 assert_template 'destroy'
1308 assert_template 'destroy'
1301 assert_not_nil assigns(:hours)
1309 assert_not_nil assigns(:hours)
1302 assert Issue.find_by_id(1) && Issue.find_by_id(3)
1310 assert Issue.find_by_id(1) && Issue.find_by_id(3)
1303 end
1311 end
1304
1312
1305 def test_destroy_issues_and_destroy_time_entries
1313 def test_destroy_issues_and_destroy_time_entries
1306 @request.session[:user_id] = 2
1314 @request.session[:user_id] = 2
1307 post :destroy, :ids => [1, 3], :todo => 'destroy'
1315 post :destroy, :ids => [1, 3], :todo => 'destroy'
1308 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1316 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1309 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1317 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1310 assert_nil TimeEntry.find_by_id([1, 2])
1318 assert_nil TimeEntry.find_by_id([1, 2])
1311 end
1319 end
1312
1320
1313 def test_destroy_issues_and_assign_time_entries_to_project
1321 def test_destroy_issues_and_assign_time_entries_to_project
1314 @request.session[:user_id] = 2
1322 @request.session[:user_id] = 2
1315 post :destroy, :ids => [1, 3], :todo => 'nullify'
1323 post :destroy, :ids => [1, 3], :todo => 'nullify'
1316 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1324 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1317 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1325 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1318 assert_nil TimeEntry.find(1).issue_id
1326 assert_nil TimeEntry.find(1).issue_id
1319 assert_nil TimeEntry.find(2).issue_id
1327 assert_nil TimeEntry.find(2).issue_id
1320 end
1328 end
1321
1329
1322 def test_destroy_issues_and_reassign_time_entries_to_another_issue
1330 def test_destroy_issues_and_reassign_time_entries_to_another_issue
1323 @request.session[:user_id] = 2
1331 @request.session[:user_id] = 2
1324 post :destroy, :ids => [1, 3], :todo => 'reassign', :reassign_to_id => 2
1332 post :destroy, :ids => [1, 3], :todo => 'reassign', :reassign_to_id => 2
1325 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1333 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1326 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1334 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1327 assert_equal 2, TimeEntry.find(1).issue_id
1335 assert_equal 2, TimeEntry.find(1).issue_id
1328 assert_equal 2, TimeEntry.find(2).issue_id
1336 assert_equal 2, TimeEntry.find(2).issue_id
1329 end
1337 end
1330
1338
1331 def test_default_search_scope
1339 def test_default_search_scope
1332 get :index
1340 get :index
1333 assert_tag :div, :attributes => {:id => 'quick-search'},
1341 assert_tag :div, :attributes => {:id => 'quick-search'},
1334 :child => {:tag => 'form',
1342 :child => {:tag => 'form',
1335 :child => {:tag => 'input', :attributes => {:name => 'issues', :type => 'hidden', :value => '1'}}}
1343 :child => {:tag => 'input', :attributes => {:name => 'issues', :type => 'hidden', :value => '1'}}}
1336 end
1344 end
1337 end
1345 end
General Comments 0
You need to be logged in to leave comments. Login now