##// END OF EJS Templates
Better handling of attachments when issue validation fails (#10253)....
Jean-Philippe Lang -
r8771:d4e6355eb3f2
parent child
Show More
@@ -1,428 +1,429
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class IssuesController < ApplicationController
18 class IssuesController < ApplicationController
19 menu_item :new_issue, :only => [:new, :create]
19 menu_item :new_issue, :only => [:new, :create]
20 default_search_scope :issues
20 default_search_scope :issues
21
21
22 before_filter :find_issue, :only => [:show, :edit, :update]
22 before_filter :find_issue, :only => [:show, :edit, :update]
23 before_filter :find_issues, :only => [:bulk_edit, :bulk_update, :move, :perform_move, :destroy]
23 before_filter :find_issues, :only => [:bulk_edit, :bulk_update, :move, :perform_move, :destroy]
24 before_filter :check_project_uniqueness, :only => [:move, :perform_move]
24 before_filter :check_project_uniqueness, :only => [:move, :perform_move]
25 before_filter :find_project, :only => [:new, :create]
25 before_filter :find_project, :only => [:new, :create]
26 before_filter :authorize, :except => [:index]
26 before_filter :authorize, :except => [:index]
27 before_filter :find_optional_project, :only => [:index]
27 before_filter :find_optional_project, :only => [:index]
28 before_filter :check_for_default_issue_status, :only => [:new, :create]
28 before_filter :check_for_default_issue_status, :only => [:new, :create]
29 before_filter :build_new_issue_from_params, :only => [:new, :create]
29 before_filter :build_new_issue_from_params, :only => [:new, :create]
30 accept_rss_auth :index, :show
30 accept_rss_auth :index, :show
31 accept_api_auth :index, :show, :create, :update, :destroy
31 accept_api_auth :index, :show, :create, :update, :destroy
32
32
33 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
33 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
34
34
35 helper :journals
35 helper :journals
36 helper :projects
36 helper :projects
37 include ProjectsHelper
37 include ProjectsHelper
38 helper :custom_fields
38 helper :custom_fields
39 include CustomFieldsHelper
39 include CustomFieldsHelper
40 helper :issue_relations
40 helper :issue_relations
41 include IssueRelationsHelper
41 include IssueRelationsHelper
42 helper :watchers
42 helper :watchers
43 include WatchersHelper
43 include WatchersHelper
44 helper :attachments
44 helper :attachments
45 include AttachmentsHelper
45 include AttachmentsHelper
46 helper :queries
46 helper :queries
47 include QueriesHelper
47 include QueriesHelper
48 helper :repositories
48 helper :repositories
49 include RepositoriesHelper
49 include RepositoriesHelper
50 helper :sort
50 helper :sort
51 include SortHelper
51 include SortHelper
52 include IssuesHelper
52 include IssuesHelper
53 helper :timelog
53 helper :timelog
54 helper :gantt
54 helper :gantt
55 include Redmine::Export::PDF
55 include Redmine::Export::PDF
56
56
57 verify :method => :post, :only => :create, :render => {:nothing => true, :status => :method_not_allowed }
57 verify :method => :post, :only => :create, :render => {:nothing => true, :status => :method_not_allowed }
58 verify :method => :post, :only => :bulk_update, :render => {:nothing => true, :status => :method_not_allowed }
58 verify :method => :post, :only => :bulk_update, :render => {:nothing => true, :status => :method_not_allowed }
59 verify :method => :put, :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
59 verify :method => :put, :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
60
60
61 def index
61 def index
62 retrieve_query
62 retrieve_query
63 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
63 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
64 sort_update(@query.sortable_columns)
64 sort_update(@query.sortable_columns)
65
65
66 if @query.valid?
66 if @query.valid?
67 case params[:format]
67 case params[:format]
68 when 'csv', 'pdf'
68 when 'csv', 'pdf'
69 @limit = Setting.issues_export_limit.to_i
69 @limit = Setting.issues_export_limit.to_i
70 when 'atom'
70 when 'atom'
71 @limit = Setting.feeds_limit.to_i
71 @limit = Setting.feeds_limit.to_i
72 when 'xml', 'json'
72 when 'xml', 'json'
73 @offset, @limit = api_offset_and_limit
73 @offset, @limit = api_offset_and_limit
74 else
74 else
75 @limit = per_page_option
75 @limit = per_page_option
76 end
76 end
77
77
78 @issue_count = @query.issue_count
78 @issue_count = @query.issue_count
79 @issue_pages = Paginator.new self, @issue_count, @limit, params['page']
79 @issue_pages = Paginator.new self, @issue_count, @limit, params['page']
80 @offset ||= @issue_pages.current.offset
80 @offset ||= @issue_pages.current.offset
81 @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
81 @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
82 :order => sort_clause,
82 :order => sort_clause,
83 :offset => @offset,
83 :offset => @offset,
84 :limit => @limit)
84 :limit => @limit)
85 @issue_count_by_group = @query.issue_count_by_group
85 @issue_count_by_group = @query.issue_count_by_group
86
86
87 respond_to do |format|
87 respond_to do |format|
88 format.html { render :template => 'issues/index', :layout => !request.xhr? }
88 format.html { render :template => 'issues/index', :layout => !request.xhr? }
89 format.api {
89 format.api {
90 Issue.load_relations(@issues) if include_in_api_response?('relations')
90 Issue.load_relations(@issues) if include_in_api_response?('relations')
91 }
91 }
92 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
92 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
93 format.csv { send_data(issues_to_csv(@issues, @project, @query, params), :type => 'text/csv; header=present', :filename => 'export.csv') }
93 format.csv { send_data(issues_to_csv(@issues, @project, @query, params), :type => 'text/csv; header=present', :filename => 'export.csv') }
94 format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'export.pdf') }
94 format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'export.pdf') }
95 end
95 end
96 else
96 else
97 respond_to do |format|
97 respond_to do |format|
98 format.html { render(:template => 'issues/index', :layout => !request.xhr?) }
98 format.html { render(:template => 'issues/index', :layout => !request.xhr?) }
99 format.any(:atom, :csv, :pdf) { render(:nothing => true) }
99 format.any(:atom, :csv, :pdf) { render(:nothing => true) }
100 format.api { render_validation_errors(@query) }
100 format.api { render_validation_errors(@query) }
101 end
101 end
102 end
102 end
103 rescue ActiveRecord::RecordNotFound
103 rescue ActiveRecord::RecordNotFound
104 render_404
104 render_404
105 end
105 end
106
106
107 def show
107 def show
108 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
108 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
109 @journals.each_with_index {|j,i| j.indice = i+1}
109 @journals.each_with_index {|j,i| j.indice = i+1}
110 @journals.reverse! if User.current.wants_comments_in_reverse_order?
110 @journals.reverse! if User.current.wants_comments_in_reverse_order?
111
111
112 @changesets = @issue.changesets.visible.all
112 @changesets = @issue.changesets.visible.all
113 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
113 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
114
114
115 @relations = @issue.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
115 @relations = @issue.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
116 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
116 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
117 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
117 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
118 @priorities = IssuePriority.active
118 @priorities = IssuePriority.active
119 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
119 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
120 respond_to do |format|
120 respond_to do |format|
121 format.html {
121 format.html {
122 retrieve_previous_and_next_issue_ids
122 retrieve_previous_and_next_issue_ids
123 render :template => 'issues/show'
123 render :template => 'issues/show'
124 }
124 }
125 format.api
125 format.api
126 format.atom { render :template => 'journals/index', :layout => false, :content_type => 'application/atom+xml' }
126 format.atom { render :template => 'journals/index', :layout => false, :content_type => 'application/atom+xml' }
127 format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
127 format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
128 end
128 end
129 end
129 end
130
130
131 # Add a new issue
131 # Add a new issue
132 # The new issue will be created from an existing one if copy_from parameter is given
132 # The new issue will be created from an existing one if copy_from parameter is given
133 def new
133 def new
134 respond_to do |format|
134 respond_to do |format|
135 format.html { render :action => 'new', :layout => !request.xhr? }
135 format.html { render :action => 'new', :layout => !request.xhr? }
136 format.js {
136 format.js {
137 render(:update) { |page|
137 render(:update) { |page|
138 if params[:project_change]
138 if params[:project_change]
139 page.replace_html 'all_attributes', :partial => 'form'
139 page.replace_html 'all_attributes', :partial => 'form'
140 else
140 else
141 page.replace_html 'attributes', :partial => 'attributes'
141 page.replace_html 'attributes', :partial => 'attributes'
142 end
142 end
143 m = User.current.allowed_to?(:log_time, @issue.project) ? 'show' : 'hide'
143 m = User.current.allowed_to?(:log_time, @issue.project) ? 'show' : 'hide'
144 page << "if ($('log_time')) {Element.#{m}('log_time');}"
144 page << "if ($('log_time')) {Element.#{m}('log_time');}"
145 }
145 }
146 }
146 }
147 end
147 end
148 end
148 end
149
149
150 def create
150 def create
151 call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
151 call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
152 @issue.save_attachments(params[:attachments])
152 if @issue.save
153 if @issue.save
153 attachments = Attachment.attach_files(@issue, params[:attachments])
154 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
154 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
155 respond_to do |format|
155 respond_to do |format|
156 format.html {
156 format.html {
157 render_attachment_warning_if_needed(@issue)
157 render_attachment_warning_if_needed(@issue)
158 flash[:notice] = l(:notice_issue_successful_create, :id => "<a href='#{issue_path(@issue)}'>##{@issue.id}</a>")
158 flash[:notice] = l(:notice_issue_successful_create, :id => "<a href='#{issue_path(@issue)}'>##{@issue.id}</a>")
159 redirect_to(params[:continue] ? { :action => 'new', :project_id => @issue.project, :issue => {:tracker_id => @issue.tracker, :parent_issue_id => @issue.parent_issue_id}.reject {|k,v| v.nil?} } :
159 redirect_to(params[:continue] ? { :action => 'new', :project_id => @issue.project, :issue => {:tracker_id => @issue.tracker, :parent_issue_id => @issue.parent_issue_id}.reject {|k,v| v.nil?} } :
160 { :action => 'show', :id => @issue })
160 { :action => 'show', :id => @issue })
161 }
161 }
162 format.api { render :action => 'show', :status => :created, :location => issue_url(@issue) }
162 format.api { render :action => 'show', :status => :created, :location => issue_url(@issue) }
163 end
163 end
164 return
164 return
165 else
165 else
166 respond_to do |format|
166 respond_to do |format|
167 format.html { render :action => 'new' }
167 format.html { render :action => 'new' }
168 format.api { render_validation_errors(@issue) }
168 format.api { render_validation_errors(@issue) }
169 end
169 end
170 end
170 end
171 end
171 end
172
172
173 def edit
173 def edit
174 return unless update_issue_from_params
174 return unless update_issue_from_params
175
175
176 respond_to do |format|
176 respond_to do |format|
177 format.html { }
177 format.html { }
178 format.xml { }
178 format.xml { }
179 end
179 end
180 end
180 end
181
181
182 def update
182 def update
183 return unless update_issue_from_params
183 return unless update_issue_from_params
184 @issue.save_attachments(params[:attachments])
184 saved = false
185 saved = false
185 begin
186 begin
186 saved = @issue.save_issue_with_child_records(params, @time_entry)
187 saved = @issue.save_issue_with_child_records(params, @time_entry)
187 rescue ActiveRecord::StaleObjectError
188 rescue ActiveRecord::StaleObjectError
188 @conflict = true
189 @conflict = true
189 if params[:last_journal_id]
190 if params[:last_journal_id]
190 if params[:last_journal_id].present?
191 if params[:last_journal_id].present?
191 last_journal_id = params[:last_journal_id].to_i
192 last_journal_id = params[:last_journal_id].to_i
192 @conflict_journals = @issue.journals.all(:conditions => ["#{Journal.table_name}.id > ?", last_journal_id])
193 @conflict_journals = @issue.journals.all(:conditions => ["#{Journal.table_name}.id > ?", last_journal_id])
193 else
194 else
194 @conflict_journals = @issue.journals.all
195 @conflict_journals = @issue.journals.all
195 end
196 end
196 end
197 end
197 end
198 end
198
199
199 if saved
200 if saved
200 render_attachment_warning_if_needed(@issue)
201 render_attachment_warning_if_needed(@issue)
201 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
202 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
202
203
203 respond_to do |format|
204 respond_to do |format|
204 format.html { redirect_back_or_default({:action => 'show', :id => @issue}) }
205 format.html { redirect_back_or_default({:action => 'show', :id => @issue}) }
205 format.api { head :ok }
206 format.api { head :ok }
206 end
207 end
207 else
208 else
208 respond_to do |format|
209 respond_to do |format|
209 format.html { render :action => 'edit' }
210 format.html { render :action => 'edit' }
210 format.api { render_validation_errors(@issue) }
211 format.api { render_validation_errors(@issue) }
211 end
212 end
212 end
213 end
213 end
214 end
214
215
215 # Bulk edit/copy a set of issues
216 # Bulk edit/copy a set of issues
216 def bulk_edit
217 def bulk_edit
217 @issues.sort!
218 @issues.sort!
218 @copy = params[:copy].present?
219 @copy = params[:copy].present?
219 @notes = params[:notes]
220 @notes = params[:notes]
220
221
221 if User.current.allowed_to?(:move_issues, @projects)
222 if User.current.allowed_to?(:move_issues, @projects)
222 @allowed_projects = Issue.allowed_target_projects_on_move
223 @allowed_projects = Issue.allowed_target_projects_on_move
223 if params[:issue]
224 if params[:issue]
224 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:issue][:project_id]}
225 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:issue][:project_id]}
225 if @target_project
226 if @target_project
226 target_projects = [@target_project]
227 target_projects = [@target_project]
227 end
228 end
228 end
229 end
229 end
230 end
230 target_projects ||= @projects
231 target_projects ||= @projects
231
232
232 @available_statuses = @issues.map(&:new_statuses_allowed_to).reduce(:&)
233 @available_statuses = @issues.map(&:new_statuses_allowed_to).reduce(:&)
233 @custom_fields = target_projects.map{|p|p.all_issue_custom_fields}.reduce(:&)
234 @custom_fields = target_projects.map{|p|p.all_issue_custom_fields}.reduce(:&)
234 @assignables = target_projects.map(&:assignable_users).reduce(:&)
235 @assignables = target_projects.map(&:assignable_users).reduce(:&)
235 @trackers = target_projects.map(&:trackers).reduce(:&)
236 @trackers = target_projects.map(&:trackers).reduce(:&)
236
237
237 @safe_attributes = @issues.map(&:safe_attribute_names).reduce(:&)
238 @safe_attributes = @issues.map(&:safe_attribute_names).reduce(:&)
238 render :layout => false if request.xhr?
239 render :layout => false if request.xhr?
239 end
240 end
240
241
241 def bulk_update
242 def bulk_update
242 @issues.sort!
243 @issues.sort!
243 @copy = params[:copy].present?
244 @copy = params[:copy].present?
244 attributes = parse_params_for_bulk_issue_attributes(params)
245 attributes = parse_params_for_bulk_issue_attributes(params)
245
246
246 unsaved_issue_ids = []
247 unsaved_issue_ids = []
247 moved_issues = []
248 moved_issues = []
248 @issues.each do |issue|
249 @issues.each do |issue|
249 issue.reload
250 issue.reload
250 if @copy
251 if @copy
251 issue = issue.copy
252 issue = issue.copy
252 end
253 end
253 journal = issue.init_journal(User.current, params[:notes])
254 journal = issue.init_journal(User.current, params[:notes])
254 issue.safe_attributes = attributes
255 issue.safe_attributes = attributes
255 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
256 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
256 if issue.save
257 if issue.save
257 moved_issues << issue
258 moved_issues << issue
258 else
259 else
259 # Keep unsaved issue ids to display them in flash error
260 # Keep unsaved issue ids to display them in flash error
260 unsaved_issue_ids << issue.id
261 unsaved_issue_ids << issue.id
261 end
262 end
262 end
263 end
263 set_flash_from_bulk_issue_save(@issues, unsaved_issue_ids)
264 set_flash_from_bulk_issue_save(@issues, unsaved_issue_ids)
264
265
265 if params[:follow]
266 if params[:follow]
266 if @issues.size == 1 && moved_issues.size == 1
267 if @issues.size == 1 && moved_issues.size == 1
267 redirect_to :controller => 'issues', :action => 'show', :id => moved_issues.first
268 redirect_to :controller => 'issues', :action => 'show', :id => moved_issues.first
268 elsif moved_issues.map(&:project).uniq.size == 1
269 elsif moved_issues.map(&:project).uniq.size == 1
269 redirect_to :controller => 'issues', :action => 'index', :project_id => moved_issues.map(&:project).first
270 redirect_to :controller => 'issues', :action => 'index', :project_id => moved_issues.map(&:project).first
270 end
271 end
271 else
272 else
272 redirect_back_or_default({:controller => 'issues', :action => 'index', :project_id => @project})
273 redirect_back_or_default({:controller => 'issues', :action => 'index', :project_id => @project})
273 end
274 end
274 end
275 end
275
276
276 verify :method => :delete, :only => :destroy, :render => { :nothing => true, :status => :method_not_allowed }
277 verify :method => :delete, :only => :destroy, :render => { :nothing => true, :status => :method_not_allowed }
277 def destroy
278 def destroy
278 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
279 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
279 if @hours > 0
280 if @hours > 0
280 case params[:todo]
281 case params[:todo]
281 when 'destroy'
282 when 'destroy'
282 # nothing to do
283 # nothing to do
283 when 'nullify'
284 when 'nullify'
284 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
285 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
285 when 'reassign'
286 when 'reassign'
286 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
287 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
287 if reassign_to.nil?
288 if reassign_to.nil?
288 flash.now[:error] = l(:error_issue_not_found_in_project)
289 flash.now[:error] = l(:error_issue_not_found_in_project)
289 return
290 return
290 else
291 else
291 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
292 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
292 end
293 end
293 else
294 else
294 # display the destroy form if it's a user request
295 # display the destroy form if it's a user request
295 return unless api_request?
296 return unless api_request?
296 end
297 end
297 end
298 end
298 @issues.each do |issue|
299 @issues.each do |issue|
299 begin
300 begin
300 issue.reload.destroy
301 issue.reload.destroy
301 rescue ::ActiveRecord::RecordNotFound # raised by #reload if issue no longer exists
302 rescue ::ActiveRecord::RecordNotFound # raised by #reload if issue no longer exists
302 # nothing to do, issue was already deleted (eg. by a parent)
303 # nothing to do, issue was already deleted (eg. by a parent)
303 end
304 end
304 end
305 end
305 respond_to do |format|
306 respond_to do |format|
306 format.html { redirect_back_or_default(:action => 'index', :project_id => @project) }
307 format.html { redirect_back_or_default(:action => 'index', :project_id => @project) }
307 format.api { head :ok }
308 format.api { head :ok }
308 end
309 end
309 end
310 end
310
311
311 private
312 private
312 def find_issue
313 def find_issue
313 # Issue.visible.find(...) can not be used to redirect user to the login form
314 # Issue.visible.find(...) can not be used to redirect user to the login form
314 # if the issue actually exists but requires authentication
315 # if the issue actually exists but requires authentication
315 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
316 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
316 unless @issue.visible?
317 unless @issue.visible?
317 deny_access
318 deny_access
318 return
319 return
319 end
320 end
320 @project = @issue.project
321 @project = @issue.project
321 rescue ActiveRecord::RecordNotFound
322 rescue ActiveRecord::RecordNotFound
322 render_404
323 render_404
323 end
324 end
324
325
325 def find_project
326 def find_project
326 project_id = params[:project_id] || (params[:issue] && params[:issue][:project_id])
327 project_id = params[:project_id] || (params[:issue] && params[:issue][:project_id])
327 @project = Project.find(project_id)
328 @project = Project.find(project_id)
328 rescue ActiveRecord::RecordNotFound
329 rescue ActiveRecord::RecordNotFound
329 render_404
330 render_404
330 end
331 end
331
332
332 def retrieve_previous_and_next_issue_ids
333 def retrieve_previous_and_next_issue_ids
333 retrieve_query_from_session
334 retrieve_query_from_session
334 if @query
335 if @query
335 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
336 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
336 sort_update(@query.sortable_columns, 'issues_index_sort')
337 sort_update(@query.sortable_columns, 'issues_index_sort')
337 limit = 500
338 limit = 500
338 issue_ids = @query.issue_ids(:order => sort_clause, :limit => (limit + 1), :include => [:assigned_to, :tracker, :priority, :category, :fixed_version])
339 issue_ids = @query.issue_ids(:order => sort_clause, :limit => (limit + 1), :include => [:assigned_to, :tracker, :priority, :category, :fixed_version])
339 if (idx = issue_ids.index(@issue.id)) && idx < limit
340 if (idx = issue_ids.index(@issue.id)) && idx < limit
340 if issue_ids.size < 500
341 if issue_ids.size < 500
341 @issue_position = idx + 1
342 @issue_position = idx + 1
342 @issue_count = issue_ids.size
343 @issue_count = issue_ids.size
343 end
344 end
344 @prev_issue_id = issue_ids[idx - 1] if idx > 0
345 @prev_issue_id = issue_ids[idx - 1] if idx > 0
345 @next_issue_id = issue_ids[idx + 1] if idx < (issue_ids.size - 1)
346 @next_issue_id = issue_ids[idx + 1] if idx < (issue_ids.size - 1)
346 end
347 end
347 end
348 end
348 end
349 end
349
350
350 # Used by #edit and #update to set some common instance variables
351 # Used by #edit and #update to set some common instance variables
351 # from the params
352 # from the params
352 # TODO: Refactor, not everything in here is needed by #edit
353 # TODO: Refactor, not everything in here is needed by #edit
353 def update_issue_from_params
354 def update_issue_from_params
354 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
355 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
355 @priorities = IssuePriority.active
356 @priorities = IssuePriority.active
356 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
357 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
357 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
358 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
358 @time_entry.attributes = params[:time_entry]
359 @time_entry.attributes = params[:time_entry]
359
360
360 @notes = params[:notes] || (params[:issue].present? ? params[:issue][:notes] : nil)
361 @notes = params[:notes] || (params[:issue].present? ? params[:issue][:notes] : nil)
361 @issue.init_journal(User.current, @notes)
362 @issue.init_journal(User.current, @notes)
362
363
363 issue_attributes = params[:issue]
364 issue_attributes = params[:issue]
364 if issue_attributes && params[:conflict_resolution]
365 if issue_attributes && params[:conflict_resolution]
365 case params[:conflict_resolution]
366 case params[:conflict_resolution]
366 when 'overwrite'
367 when 'overwrite'
367 issue_attributes = issue_attributes.dup
368 issue_attributes = issue_attributes.dup
368 issue_attributes.delete(:lock_version)
369 issue_attributes.delete(:lock_version)
369 when 'add_notes'
370 when 'add_notes'
370 issue_attributes = {}
371 issue_attributes = {}
371 when 'cancel'
372 when 'cancel'
372 redirect_to issue_path(@issue)
373 redirect_to issue_path(@issue)
373 return false
374 return false
374 end
375 end
375 end
376 end
376 @issue.safe_attributes = issue_attributes
377 @issue.safe_attributes = issue_attributes
377 true
378 true
378 end
379 end
379
380
380 # TODO: Refactor, lots of extra code in here
381 # TODO: Refactor, lots of extra code in here
381 # TODO: Changing tracker on an existing issue should not trigger this
382 # TODO: Changing tracker on an existing issue should not trigger this
382 def build_new_issue_from_params
383 def build_new_issue_from_params
383 if params[:id].blank?
384 if params[:id].blank?
384 @issue = Issue.new
385 @issue = Issue.new
385 if params[:copy_from]
386 if params[:copy_from]
386 begin
387 begin
387 @copy_from = Issue.visible.find(params[:copy_from])
388 @copy_from = Issue.visible.find(params[:copy_from])
388 @copy_attachments = params[:copy_attachments].present? || request.get?
389 @copy_attachments = params[:copy_attachments].present? || request.get?
389 @issue.copy_from(@copy_from, :attachments => @copy_attachments)
390 @issue.copy_from(@copy_from, :attachments => @copy_attachments)
390 rescue ActiveRecord::RecordNotFound
391 rescue ActiveRecord::RecordNotFound
391 render_404
392 render_404
392 return
393 return
393 end
394 end
394 end
395 end
395 @issue.project = @project
396 @issue.project = @project
396 else
397 else
397 @issue = @project.issues.visible.find(params[:id])
398 @issue = @project.issues.visible.find(params[:id])
398 end
399 end
399
400
400 @issue.project = @project
401 @issue.project = @project
401 @issue.author = User.current
402 @issue.author = User.current
402 # Tracker must be set before custom field values
403 # Tracker must be set before custom field values
403 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
404 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
404 if @issue.tracker.nil?
405 if @issue.tracker.nil?
405 render_error l(:error_no_tracker_in_project)
406 render_error l(:error_no_tracker_in_project)
406 return false
407 return false
407 end
408 end
408 @issue.start_date ||= Date.today if Setting.default_issue_start_date_to_creation_date?
409 @issue.start_date ||= Date.today if Setting.default_issue_start_date_to_creation_date?
409 @issue.safe_attributes = params[:issue]
410 @issue.safe_attributes = params[:issue]
410
411
411 @priorities = IssuePriority.active
412 @priorities = IssuePriority.active
412 @allowed_statuses = @issue.new_statuses_allowed_to(User.current, true)
413 @allowed_statuses = @issue.new_statuses_allowed_to(User.current, true)
413 end
414 end
414
415
415 def check_for_default_issue_status
416 def check_for_default_issue_status
416 if IssueStatus.default.nil?
417 if IssueStatus.default.nil?
417 render_error l(:error_no_default_issue_status)
418 render_error l(:error_no_default_issue_status)
418 return false
419 return false
419 end
420 end
420 end
421 end
421
422
422 def parse_params_for_bulk_issue_attributes(params)
423 def parse_params_for_bulk_issue_attributes(params)
423 attributes = (params[:issue] || {}).reject {|k,v| v.blank?}
424 attributes = (params[:issue] || {}).reject {|k,v| v.blank?}
424 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
425 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
425 attributes[:custom_field_values].reject! {|k,v| v.blank?} if attributes[:custom_field_values]
426 attributes[:custom_field_values].reject! {|k,v| v.blank?} if attributes[:custom_field_values]
426 attributes
427 attributes
427 end
428 end
428 end
429 end
@@ -1,219 +1,232
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 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 "digest/md5"
18 require "digest/md5"
19
19
20 class Attachment < ActiveRecord::Base
20 class Attachment < ActiveRecord::Base
21 belongs_to :container, :polymorphic => true
21 belongs_to :container, :polymorphic => true
22 belongs_to :author, :class_name => "User", :foreign_key => "author_id"
22 belongs_to :author, :class_name => "User", :foreign_key => "author_id"
23
23
24 validates_presence_of :container, :filename, :author
24 validates_presence_of :filename, :author
25 validates_length_of :filename, :maximum => 255
25 validates_length_of :filename, :maximum => 255
26 validates_length_of :disk_filename, :maximum => 255
26 validates_length_of :disk_filename, :maximum => 255
27 validate :validate_max_file_size
27 validate :validate_max_file_size
28
28
29 acts_as_event :title => :filename,
29 acts_as_event :title => :filename,
30 :url => Proc.new {|o| {:controller => 'attachments', :action => 'download', :id => o.id, :filename => o.filename}}
30 :url => Proc.new {|o| {:controller => 'attachments', :action => 'download', :id => o.id, :filename => o.filename}}
31
31
32 acts_as_activity_provider :type => 'files',
32 acts_as_activity_provider :type => 'files',
33 :permission => :view_files,
33 :permission => :view_files,
34 :author_key => :author_id,
34 :author_key => :author_id,
35 :find_options => {:select => "#{Attachment.table_name}.*",
35 :find_options => {:select => "#{Attachment.table_name}.*",
36 :joins => "LEFT JOIN #{Version.table_name} ON #{Attachment.table_name}.container_type='Version' AND #{Version.table_name}.id = #{Attachment.table_name}.container_id " +
36 :joins => "LEFT JOIN #{Version.table_name} ON #{Attachment.table_name}.container_type='Version' AND #{Version.table_name}.id = #{Attachment.table_name}.container_id " +
37 "LEFT JOIN #{Project.table_name} ON #{Version.table_name}.project_id = #{Project.table_name}.id OR ( #{Attachment.table_name}.container_type='Project' AND #{Attachment.table_name}.container_id = #{Project.table_name}.id )"}
37 "LEFT JOIN #{Project.table_name} ON #{Version.table_name}.project_id = #{Project.table_name}.id OR ( #{Attachment.table_name}.container_type='Project' AND #{Attachment.table_name}.container_id = #{Project.table_name}.id )"}
38
38
39 acts_as_activity_provider :type => 'documents',
39 acts_as_activity_provider :type => 'documents',
40 :permission => :view_documents,
40 :permission => :view_documents,
41 :author_key => :author_id,
41 :author_key => :author_id,
42 :find_options => {:select => "#{Attachment.table_name}.*",
42 :find_options => {:select => "#{Attachment.table_name}.*",
43 :joins => "LEFT JOIN #{Document.table_name} ON #{Attachment.table_name}.container_type='Document' AND #{Document.table_name}.id = #{Attachment.table_name}.container_id " +
43 :joins => "LEFT JOIN #{Document.table_name} ON #{Attachment.table_name}.container_type='Document' AND #{Document.table_name}.id = #{Attachment.table_name}.container_id " +
44 "LEFT JOIN #{Project.table_name} ON #{Document.table_name}.project_id = #{Project.table_name}.id"}
44 "LEFT JOIN #{Project.table_name} ON #{Document.table_name}.project_id = #{Project.table_name}.id"}
45
45
46 cattr_accessor :storage_path
46 cattr_accessor :storage_path
47 @@storage_path = Redmine::Configuration['attachments_storage_path'] || "#{Rails.root}/files"
47 @@storage_path = Redmine::Configuration['attachments_storage_path'] || "#{Rails.root}/files"
48
48
49 before_save :files_to_final_location
49 before_save :files_to_final_location
50 after_destroy :delete_from_disk
50 after_destroy :delete_from_disk
51
51
52 def container_with_blank_type_check
53 if container_type.blank?
54 nil
55 else
56 container_without_blank_type_check
57 end
58 end
59 alias_method_chain :container, :blank_type_check unless method_defined?(:container_without_blank_type_check)
60
52 # Returns an unsaved copy of the attachment
61 # Returns an unsaved copy of the attachment
53 def copy(attributes=nil)
62 def copy(attributes=nil)
54 copy = self.class.new
63 copy = self.class.new
55 copy.attributes = self.attributes.dup.except("id", "downloads")
64 copy.attributes = self.attributes.dup.except("id", "downloads")
56 copy.attributes = attributes if attributes
65 copy.attributes = attributes if attributes
57 copy
66 copy
58 end
67 end
59
68
60 def validate_max_file_size
69 def validate_max_file_size
61 if @temp_file && self.filesize > Setting.attachment_max_size.to_i.kilobytes
70 if @temp_file && self.filesize > Setting.attachment_max_size.to_i.kilobytes
62 errors.add(:base, :too_long, :count => Setting.attachment_max_size.to_i.kilobytes)
71 errors.add(:base, :too_long, :count => Setting.attachment_max_size.to_i.kilobytes)
63 end
72 end
64 end
73 end
65
74
66 def file=(incoming_file)
75 def file=(incoming_file)
67 unless incoming_file.nil?
76 unless incoming_file.nil?
68 @temp_file = incoming_file
77 @temp_file = incoming_file
69 if @temp_file.size > 0
78 if @temp_file.size > 0
70 self.filename = sanitize_filename(@temp_file.original_filename)
79 self.filename = sanitize_filename(@temp_file.original_filename)
71 self.disk_filename = Attachment.disk_filename(filename)
80 self.disk_filename = Attachment.disk_filename(filename)
72 self.content_type = @temp_file.content_type.to_s.chomp
81 self.content_type = @temp_file.content_type.to_s.chomp
73 if content_type.blank?
82 if content_type.blank?
74 self.content_type = Redmine::MimeType.of(filename)
83 self.content_type = Redmine::MimeType.of(filename)
75 end
84 end
76 self.filesize = @temp_file.size
85 self.filesize = @temp_file.size
77 end
86 end
78 end
87 end
79 end
88 end
80
89
81 def file
90 def file
82 nil
91 nil
83 end
92 end
84
93
85 # Copies the temporary file to its final location
94 # Copies the temporary file to its final location
86 # and computes its MD5 hash
95 # and computes its MD5 hash
87 def files_to_final_location
96 def files_to_final_location
88 if @temp_file && (@temp_file.size > 0)
97 if @temp_file && (@temp_file.size > 0)
89 logger.info("Saving attachment '#{self.diskfile}' (#{@temp_file.size} bytes)")
98 logger.info("Saving attachment '#{self.diskfile}' (#{@temp_file.size} bytes)")
90 md5 = Digest::MD5.new
99 md5 = Digest::MD5.new
91 File.open(diskfile, "wb") do |f|
100 File.open(diskfile, "wb") do |f|
92 buffer = ""
101 buffer = ""
93 while (buffer = @temp_file.read(8192))
102 while (buffer = @temp_file.read(8192))
94 f.write(buffer)
103 f.write(buffer)
95 md5.update(buffer)
104 md5.update(buffer)
96 end
105 end
97 end
106 end
98 self.digest = md5.hexdigest
107 self.digest = md5.hexdigest
99 end
108 end
100 @temp_file = nil
109 @temp_file = nil
101 # Don't save the content type if it's longer than the authorized length
110 # Don't save the content type if it's longer than the authorized length
102 if self.content_type && self.content_type.length > 255
111 if self.content_type && self.content_type.length > 255
103 self.content_type = nil
112 self.content_type = nil
104 end
113 end
105 end
114 end
106
115
107 # Deletes the file from the file system if it's not referenced by other attachments
116 # Deletes the file from the file system if it's not referenced by other attachments
108 def delete_from_disk
117 def delete_from_disk
109 if Attachment.first(:conditions => ["disk_filename = ? AND id <> ?", disk_filename, id]).nil?
118 if Attachment.first(:conditions => ["disk_filename = ? AND id <> ?", disk_filename, id]).nil?
110 delete_from_disk!
119 delete_from_disk!
111 end
120 end
112 end
121 end
113
122
114 # Returns file's location on disk
123 # Returns file's location on disk
115 def diskfile
124 def diskfile
116 "#{@@storage_path}/#{self.disk_filename}"
125 "#{@@storage_path}/#{self.disk_filename}"
117 end
126 end
118
127
119 def increment_download
128 def increment_download
120 increment!(:downloads)
129 increment!(:downloads)
121 end
130 end
122
131
123 def project
132 def project
124 container.project
133 container.try(:project)
125 end
134 end
126
135
127 def visible?(user=User.current)
136 def visible?(user=User.current)
128 container.attachments_visible?(user)
137 container && container.attachments_visible?(user)
129 end
138 end
130
139
131 def deletable?(user=User.current)
140 def deletable?(user=User.current)
132 container.attachments_deletable?(user)
141 container && container.attachments_deletable?(user)
133 end
142 end
134
143
135 def image?
144 def image?
136 self.filename =~ /\.(bmp|gif|jpg|jpe|jpeg|png)$/i
145 self.filename =~ /\.(bmp|gif|jpg|jpe|jpeg|png)$/i
137 end
146 end
138
147
139 def is_text?
148 def is_text?
140 Redmine::MimeType.is_type?('text', filename)
149 Redmine::MimeType.is_type?('text', filename)
141 end
150 end
142
151
143 def is_diff?
152 def is_diff?
144 self.filename =~ /\.(patch|diff)$/i
153 self.filename =~ /\.(patch|diff)$/i
145 end
154 end
146
155
147 # Returns true if the file is readable
156 # Returns true if the file is readable
148 def readable?
157 def readable?
149 File.readable?(diskfile)
158 File.readable?(diskfile)
150 end
159 end
151
160
161 # Returns the attachment token
162 def token
163 "#{id}.#{digest}"
164 end
165
166 # Finds an attachment that matches the given token and that has no container
167 def self.find_by_token(token)
168 if token.to_s =~ /^(\d+)\.([0-9a-f]+)$/
169 attachment_id, attachment_digest = $1, $2
170 attachment = Attachment.first(:conditions => {:id => attachment_id, :digest => attachment_digest})
171 if attachment && attachment.container.nil?
172 attachment
173 end
174 end
175 end
176
152 # Bulk attaches a set of files to an object
177 # Bulk attaches a set of files to an object
153 #
178 #
154 # Returns a Hash of the results:
179 # Returns a Hash of the results:
155 # :files => array of the attached files
180 # :files => array of the attached files
156 # :unsaved => array of the files that could not be attached
181 # :unsaved => array of the files that could not be attached
157 def self.attach_files(obj, attachments)
182 def self.attach_files(obj, attachments)
158 attached = []
183 result = obj.save_attachments(attachments, User.current)
159 if attachments && attachments.is_a?(Hash)
184 obj.attach_saved_attachments
160 attachments.each_value do |attachment|
185 result
161 file = attachment['file']
162 next unless file && file.size > 0
163 a = Attachment.create(:container => obj,
164 :file => file,
165 :description => attachment['description'].to_s.strip,
166 :author => User.current)
167 obj.attachments << a
168
169 if a.new_record?
170 obj.unsaved_attachments ||= []
171 obj.unsaved_attachments << a
172 else
173 attached << a
174 end
175 end
176 end
177 {:files => attached, :unsaved => obj.unsaved_attachments}
178 end
186 end
179
187
180 def self.latest_attach(attachments, filename)
188 def self.latest_attach(attachments, filename)
181 attachments.sort_by(&:created_on).reverse.detect {
189 attachments.sort_by(&:created_on).reverse.detect {
182 |att| att.filename.downcase == filename.downcase
190 |att| att.filename.downcase == filename.downcase
183 }
191 }
184 end
192 end
185
193
194 def self.prune(age=1.day)
195 attachments = Attachment.all(:conditions => ["created_on < ? AND (container_type IS NULL OR container_type = ''", Time.now - age])
196 attachments.each(&:destroy)
197 end
198
186 private
199 private
187
200
188 # Physically deletes the file from the file system
201 # Physically deletes the file from the file system
189 def delete_from_disk!
202 def delete_from_disk!
190 if disk_filename.present? && File.exist?(diskfile)
203 if disk_filename.present? && File.exist?(diskfile)
191 File.delete(diskfile)
204 File.delete(diskfile)
192 end
205 end
193 end
206 end
194
207
195 def sanitize_filename(value)
208 def sanitize_filename(value)
196 # get only the filename, not the whole path
209 # get only the filename, not the whole path
197 just_filename = value.gsub(/^.*(\\|\/)/, '')
210 just_filename = value.gsub(/^.*(\\|\/)/, '')
198
211
199 # Finally, replace invalid characters with underscore
212 # Finally, replace invalid characters with underscore
200 @filename = just_filename.gsub(/[\/\?\%\*\:\|\"\'<>]+/, '_')
213 @filename = just_filename.gsub(/[\/\?\%\*\:\|\"\'<>]+/, '_')
201 end
214 end
202
215
203 # Returns an ASCII or hashed filename
216 # Returns an ASCII or hashed filename
204 def self.disk_filename(filename)
217 def self.disk_filename(filename)
205 timestamp = DateTime.now.strftime("%y%m%d%H%M%S")
218 timestamp = DateTime.now.strftime("%y%m%d%H%M%S")
206 ascii = ''
219 ascii = ''
207 if filename =~ %r{^[a-zA-Z0-9_\.\-]*$}
220 if filename =~ %r{^[a-zA-Z0-9_\.\-]*$}
208 ascii = filename
221 ascii = filename
209 else
222 else
210 ascii = Digest::MD5.hexdigest(filename)
223 ascii = Digest::MD5.hexdigest(filename)
211 # keep the extension if any
224 # keep the extension if any
212 ascii << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$}
225 ascii << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$}
213 end
226 end
214 while File.exist?(File.join(@@storage_path, "#{timestamp}_#{ascii}"))
227 while File.exist?(File.join(@@storage_path, "#{timestamp}_#{ascii}"))
215 timestamp.succ!
228 timestamp.succ!
216 end
229 end
217 "#{timestamp}_#{ascii}"
230 "#{timestamp}_#{ascii}"
218 end
231 end
219 end
232 end
@@ -1,1087 +1,1078
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class Issue < ActiveRecord::Base
18 class Issue < ActiveRecord::Base
19 include Redmine::SafeAttributes
19 include Redmine::SafeAttributes
20
20
21 belongs_to :project
21 belongs_to :project
22 belongs_to :tracker
22 belongs_to :tracker
23 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
23 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
24 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
24 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
25 belongs_to :assigned_to, :class_name => 'Principal', :foreign_key => 'assigned_to_id'
25 belongs_to :assigned_to, :class_name => 'Principal', :foreign_key => 'assigned_to_id'
26 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
26 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
27 belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
27 belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
28 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
28 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
29
29
30 has_many :journals, :as => :journalized, :dependent => :destroy
30 has_many :journals, :as => :journalized, :dependent => :destroy
31 has_many :time_entries, :dependent => :delete_all
31 has_many :time_entries, :dependent => :delete_all
32 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
32 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
33
33
34 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
34 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
35 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
35 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
36
36
37 acts_as_nested_set :scope => 'root_id', :dependent => :destroy
37 acts_as_nested_set :scope => 'root_id', :dependent => :destroy
38 acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed
38 acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed
39 acts_as_customizable
39 acts_as_customizable
40 acts_as_watchable
40 acts_as_watchable
41 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
41 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
42 :include => [:project, :journals],
42 :include => [:project, :journals],
43 # sort by id so that limited eager loading doesn't break with postgresql
43 # sort by id so that limited eager loading doesn't break with postgresql
44 :order_column => "#{table_name}.id"
44 :order_column => "#{table_name}.id"
45 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
45 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
46 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
46 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
47 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
47 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
48
48
49 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
49 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
50 :author_key => :author_id
50 :author_key => :author_id
51
51
52 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
52 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
53
53
54 attr_reader :current_journal
54 attr_reader :current_journal
55
55
56 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
56 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
57
57
58 validates_length_of :subject, :maximum => 255
58 validates_length_of :subject, :maximum => 255
59 validates_inclusion_of :done_ratio, :in => 0..100
59 validates_inclusion_of :done_ratio, :in => 0..100
60 validates_numericality_of :estimated_hours, :allow_nil => true
60 validates_numericality_of :estimated_hours, :allow_nil => true
61 validate :validate_issue
61 validate :validate_issue
62
62
63 named_scope :visible, lambda {|*args| { :include => :project,
63 named_scope :visible, lambda {|*args| { :include => :project,
64 :conditions => Issue.visible_condition(args.shift || User.current, *args) } }
64 :conditions => Issue.visible_condition(args.shift || User.current, *args) } }
65
65
66 named_scope :open, lambda {|*args|
66 named_scope :open, lambda {|*args|
67 is_closed = args.size > 0 ? !args.first : false
67 is_closed = args.size > 0 ? !args.first : false
68 {:conditions => ["#{IssueStatus.table_name}.is_closed = ?", is_closed], :include => :status}
68 {:conditions => ["#{IssueStatus.table_name}.is_closed = ?", is_closed], :include => :status}
69 }
69 }
70
70
71 named_scope :recently_updated, :order => "#{Issue.table_name}.updated_on DESC"
71 named_scope :recently_updated, :order => "#{Issue.table_name}.updated_on DESC"
72 named_scope :with_limit, lambda { |limit| { :limit => limit} }
72 named_scope :with_limit, lambda { |limit| { :limit => limit} }
73 named_scope :on_active_project, :include => [:status, :project, :tracker],
73 named_scope :on_active_project, :include => [:status, :project, :tracker],
74 :conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"]
74 :conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"]
75
75
76 before_create :default_assign
76 before_create :default_assign
77 before_save :close_duplicates, :update_done_ratio_from_issue_status
77 before_save :close_duplicates, :update_done_ratio_from_issue_status
78 after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?}
78 after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?}
79 after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal
79 after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal
80 after_destroy :update_parent_attributes
80 after_destroy :update_parent_attributes
81
81
82 # Returns a SQL conditions string used to find all issues visible by the specified user
82 # Returns a SQL conditions string used to find all issues visible by the specified user
83 def self.visible_condition(user, options={})
83 def self.visible_condition(user, options={})
84 Project.allowed_to_condition(user, :view_issues, options) do |role, user|
84 Project.allowed_to_condition(user, :view_issues, options) do |role, user|
85 case role.issues_visibility
85 case role.issues_visibility
86 when 'all'
86 when 'all'
87 nil
87 nil
88 when 'default'
88 when 'default'
89 user_ids = [user.id] + user.groups.map(&:id)
89 user_ids = [user.id] + user.groups.map(&:id)
90 "(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
90 "(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
91 when 'own'
91 when 'own'
92 user_ids = [user.id] + user.groups.map(&:id)
92 user_ids = [user.id] + user.groups.map(&:id)
93 "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
93 "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
94 else
94 else
95 '1=0'
95 '1=0'
96 end
96 end
97 end
97 end
98 end
98 end
99
99
100 # Returns true if usr or current user is allowed to view the issue
100 # Returns true if usr or current user is allowed to view the issue
101 def visible?(usr=nil)
101 def visible?(usr=nil)
102 (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
102 (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
103 case role.issues_visibility
103 case role.issues_visibility
104 when 'all'
104 when 'all'
105 true
105 true
106 when 'default'
106 when 'default'
107 !self.is_private? || self.author == user || user.is_or_belongs_to?(assigned_to)
107 !self.is_private? || self.author == user || user.is_or_belongs_to?(assigned_to)
108 when 'own'
108 when 'own'
109 self.author == user || user.is_or_belongs_to?(assigned_to)
109 self.author == user || user.is_or_belongs_to?(assigned_to)
110 else
110 else
111 false
111 false
112 end
112 end
113 end
113 end
114 end
114 end
115
115
116 def initialize(attributes=nil, *args)
116 def initialize(attributes=nil, *args)
117 super
117 super
118 if new_record?
118 if new_record?
119 # set default values for new records only
119 # set default values for new records only
120 self.status ||= IssueStatus.default
120 self.status ||= IssueStatus.default
121 self.priority ||= IssuePriority.default
121 self.priority ||= IssuePriority.default
122 self.watcher_user_ids = []
122 self.watcher_user_ids = []
123 end
123 end
124 end
124 end
125
125
126 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
126 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
127 def available_custom_fields
127 def available_custom_fields
128 (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields.all) : []
128 (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields.all) : []
129 end
129 end
130
130
131 # Copies attributes from another issue, arg can be an id or an Issue
131 # Copies attributes from another issue, arg can be an id or an Issue
132 def copy_from(arg, options={})
132 def copy_from(arg, options={})
133 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
133 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
134 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
134 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
135 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
135 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
136 self.status = issue.status
136 self.status = issue.status
137 self.author = User.current
137 self.author = User.current
138 unless options[:attachments] == false
138 unless options[:attachments] == false
139 self.attachments = issue.attachments.map do |attachement|
139 self.attachments = issue.attachments.map do |attachement|
140 attachement.copy(:container => self)
140 attachement.copy(:container => self)
141 end
141 end
142 end
142 end
143 @copied_from = issue
143 @copied_from = issue
144 self
144 self
145 end
145 end
146
146
147 # Returns an unsaved copy of the issue
147 # Returns an unsaved copy of the issue
148 def copy(attributes=nil)
148 def copy(attributes=nil)
149 copy = self.class.new.copy_from(self)
149 copy = self.class.new.copy_from(self)
150 copy.attributes = attributes if attributes
150 copy.attributes = attributes if attributes
151 copy
151 copy
152 end
152 end
153
153
154 # Returns true if the issue is a copy
154 # Returns true if the issue is a copy
155 def copy?
155 def copy?
156 @copied_from.present?
156 @copied_from.present?
157 end
157 end
158
158
159 # Moves/copies an issue to a new project and tracker
159 # Moves/copies an issue to a new project and tracker
160 # Returns the moved/copied issue on success, false on failure
160 # Returns the moved/copied issue on success, false on failure
161 def move_to_project(new_project, new_tracker=nil, options={})
161 def move_to_project(new_project, new_tracker=nil, options={})
162 ActiveSupport::Deprecation.warn "Issue#move_to_project is deprecated, use #project= instead."
162 ActiveSupport::Deprecation.warn "Issue#move_to_project is deprecated, use #project= instead."
163
163
164 if options[:copy]
164 if options[:copy]
165 issue = self.copy
165 issue = self.copy
166 else
166 else
167 issue = self
167 issue = self
168 end
168 end
169
169
170 issue.init_journal(User.current, options[:notes])
170 issue.init_journal(User.current, options[:notes])
171
171
172 # Preserve previous behaviour
172 # Preserve previous behaviour
173 # #move_to_project doesn't change tracker automatically
173 # #move_to_project doesn't change tracker automatically
174 issue.send :project=, new_project, true
174 issue.send :project=, new_project, true
175 if new_tracker
175 if new_tracker
176 issue.tracker = new_tracker
176 issue.tracker = new_tracker
177 end
177 end
178 # Allow bulk setting of attributes on the issue
178 # Allow bulk setting of attributes on the issue
179 if options[:attributes]
179 if options[:attributes]
180 issue.attributes = options[:attributes]
180 issue.attributes = options[:attributes]
181 end
181 end
182
182
183 issue.save ? issue : false
183 issue.save ? issue : false
184 end
184 end
185
185
186 def status_id=(sid)
186 def status_id=(sid)
187 self.status = nil
187 self.status = nil
188 write_attribute(:status_id, sid)
188 write_attribute(:status_id, sid)
189 end
189 end
190
190
191 def priority_id=(pid)
191 def priority_id=(pid)
192 self.priority = nil
192 self.priority = nil
193 write_attribute(:priority_id, pid)
193 write_attribute(:priority_id, pid)
194 end
194 end
195
195
196 def category_id=(cid)
196 def category_id=(cid)
197 self.category = nil
197 self.category = nil
198 write_attribute(:category_id, cid)
198 write_attribute(:category_id, cid)
199 end
199 end
200
200
201 def fixed_version_id=(vid)
201 def fixed_version_id=(vid)
202 self.fixed_version = nil
202 self.fixed_version = nil
203 write_attribute(:fixed_version_id, vid)
203 write_attribute(:fixed_version_id, vid)
204 end
204 end
205
205
206 def tracker_id=(tid)
206 def tracker_id=(tid)
207 self.tracker = nil
207 self.tracker = nil
208 result = write_attribute(:tracker_id, tid)
208 result = write_attribute(:tracker_id, tid)
209 @custom_field_values = nil
209 @custom_field_values = nil
210 result
210 result
211 end
211 end
212
212
213 def project_id=(project_id)
213 def project_id=(project_id)
214 if project_id.to_s != self.project_id.to_s
214 if project_id.to_s != self.project_id.to_s
215 self.project = (project_id.present? ? Project.find_by_id(project_id) : nil)
215 self.project = (project_id.present? ? Project.find_by_id(project_id) : nil)
216 end
216 end
217 end
217 end
218
218
219 def project=(project, keep_tracker=false)
219 def project=(project, keep_tracker=false)
220 project_was = self.project
220 project_was = self.project
221 write_attribute(:project_id, project ? project.id : nil)
221 write_attribute(:project_id, project ? project.id : nil)
222 association_instance_set('project', project)
222 association_instance_set('project', project)
223 if project_was && project && project_was != project
223 if project_was && project && project_was != project
224 unless keep_tracker || project.trackers.include?(tracker)
224 unless keep_tracker || project.trackers.include?(tracker)
225 self.tracker = project.trackers.first
225 self.tracker = project.trackers.first
226 end
226 end
227 # Reassign to the category with same name if any
227 # Reassign to the category with same name if any
228 if category
228 if category
229 self.category = project.issue_categories.find_by_name(category.name)
229 self.category = project.issue_categories.find_by_name(category.name)
230 end
230 end
231 # Keep the fixed_version if it's still valid in the new_project
231 # Keep the fixed_version if it's still valid in the new_project
232 if fixed_version && fixed_version.project != project && !project.shared_versions.include?(fixed_version)
232 if fixed_version && fixed_version.project != project && !project.shared_versions.include?(fixed_version)
233 self.fixed_version = nil
233 self.fixed_version = nil
234 end
234 end
235 if parent && parent.project_id != project_id
235 if parent && parent.project_id != project_id
236 self.parent_issue_id = nil
236 self.parent_issue_id = nil
237 end
237 end
238 @custom_field_values = nil
238 @custom_field_values = nil
239 end
239 end
240 end
240 end
241
241
242 def description=(arg)
242 def description=(arg)
243 if arg.is_a?(String)
243 if arg.is_a?(String)
244 arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n")
244 arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n")
245 end
245 end
246 write_attribute(:description, arg)
246 write_attribute(:description, arg)
247 end
247 end
248
248
249 # Overrides attributes= so that project and tracker get assigned first
249 # Overrides attributes= so that project and tracker get assigned first
250 def attributes_with_project_and_tracker_first=(new_attributes, *args)
250 def attributes_with_project_and_tracker_first=(new_attributes, *args)
251 return if new_attributes.nil?
251 return if new_attributes.nil?
252 attrs = new_attributes.dup
252 attrs = new_attributes.dup
253 attrs.stringify_keys!
253 attrs.stringify_keys!
254
254
255 %w(project project_id tracker tracker_id).each do |attr|
255 %w(project project_id tracker tracker_id).each do |attr|
256 if attrs.has_key?(attr)
256 if attrs.has_key?(attr)
257 send "#{attr}=", attrs.delete(attr)
257 send "#{attr}=", attrs.delete(attr)
258 end
258 end
259 end
259 end
260 send :attributes_without_project_and_tracker_first=, attrs, *args
260 send :attributes_without_project_and_tracker_first=, attrs, *args
261 end
261 end
262 # Do not redefine alias chain on reload (see #4838)
262 # Do not redefine alias chain on reload (see #4838)
263 alias_method_chain(:attributes=, :project_and_tracker_first) unless method_defined?(:attributes_without_project_and_tracker_first=)
263 alias_method_chain(:attributes=, :project_and_tracker_first) unless method_defined?(:attributes_without_project_and_tracker_first=)
264
264
265 def estimated_hours=(h)
265 def estimated_hours=(h)
266 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
266 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
267 end
267 end
268
268
269 safe_attributes 'project_id',
269 safe_attributes 'project_id',
270 :if => lambda {|issue, user|
270 :if => lambda {|issue, user|
271 if issue.new_record?
271 if issue.new_record?
272 issue.copy?
272 issue.copy?
273 elsif user.allowed_to?(:move_issues, issue.project)
273 elsif user.allowed_to?(:move_issues, issue.project)
274 projects = Issue.allowed_target_projects_on_move(user)
274 projects = Issue.allowed_target_projects_on_move(user)
275 projects.include?(issue.project) && projects.size > 1
275 projects.include?(issue.project) && projects.size > 1
276 end
276 end
277 }
277 }
278
278
279 safe_attributes 'tracker_id',
279 safe_attributes 'tracker_id',
280 'status_id',
280 'status_id',
281 'category_id',
281 'category_id',
282 'assigned_to_id',
282 'assigned_to_id',
283 'priority_id',
283 'priority_id',
284 'fixed_version_id',
284 'fixed_version_id',
285 'subject',
285 'subject',
286 'description',
286 'description',
287 'start_date',
287 'start_date',
288 'due_date',
288 'due_date',
289 'done_ratio',
289 'done_ratio',
290 'estimated_hours',
290 'estimated_hours',
291 'custom_field_values',
291 'custom_field_values',
292 'custom_fields',
292 'custom_fields',
293 'lock_version',
293 'lock_version',
294 :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
294 :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
295
295
296 safe_attributes 'status_id',
296 safe_attributes 'status_id',
297 'assigned_to_id',
297 'assigned_to_id',
298 'fixed_version_id',
298 'fixed_version_id',
299 'done_ratio',
299 'done_ratio',
300 'lock_version',
300 'lock_version',
301 :if => lambda {|issue, user| issue.new_statuses_allowed_to(user).any? }
301 :if => lambda {|issue, user| issue.new_statuses_allowed_to(user).any? }
302
302
303 safe_attributes 'watcher_user_ids',
303 safe_attributes 'watcher_user_ids',
304 :if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)}
304 :if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)}
305
305
306 safe_attributes 'is_private',
306 safe_attributes 'is_private',
307 :if => lambda {|issue, user|
307 :if => lambda {|issue, user|
308 user.allowed_to?(:set_issues_private, issue.project) ||
308 user.allowed_to?(:set_issues_private, issue.project) ||
309 (issue.author == user && user.allowed_to?(:set_own_issues_private, issue.project))
309 (issue.author == user && user.allowed_to?(:set_own_issues_private, issue.project))
310 }
310 }
311
311
312 safe_attributes 'parent_issue_id',
312 safe_attributes 'parent_issue_id',
313 :if => lambda {|issue, user| (issue.new_record? || user.allowed_to?(:edit_issues, issue.project)) &&
313 :if => lambda {|issue, user| (issue.new_record? || user.allowed_to?(:edit_issues, issue.project)) &&
314 user.allowed_to?(:manage_subtasks, issue.project)}
314 user.allowed_to?(:manage_subtasks, issue.project)}
315
315
316 # Safely sets attributes
316 # Safely sets attributes
317 # Should be called from controllers instead of #attributes=
317 # Should be called from controllers instead of #attributes=
318 # attr_accessible is too rough because we still want things like
318 # attr_accessible is too rough because we still want things like
319 # Issue.new(:project => foo) to work
319 # Issue.new(:project => foo) to work
320 def safe_attributes=(attrs, user=User.current)
320 def safe_attributes=(attrs, user=User.current)
321 return unless attrs.is_a?(Hash)
321 return unless attrs.is_a?(Hash)
322
322
323 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
323 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
324 attrs = delete_unsafe_attributes(attrs, user)
324 attrs = delete_unsafe_attributes(attrs, user)
325 return if attrs.empty?
325 return if attrs.empty?
326
326
327 # Project and Tracker must be set before since new_statuses_allowed_to depends on it.
327 # Project and Tracker must be set before since new_statuses_allowed_to depends on it.
328 if p = attrs.delete('project_id')
328 if p = attrs.delete('project_id')
329 if allowed_target_projects(user).collect(&:id).include?(p.to_i)
329 if allowed_target_projects(user).collect(&:id).include?(p.to_i)
330 self.project_id = p
330 self.project_id = p
331 end
331 end
332 end
332 end
333
333
334 if t = attrs.delete('tracker_id')
334 if t = attrs.delete('tracker_id')
335 self.tracker_id = t
335 self.tracker_id = t
336 end
336 end
337
337
338 if attrs['status_id']
338 if attrs['status_id']
339 unless new_statuses_allowed_to(user).collect(&:id).include?(attrs['status_id'].to_i)
339 unless new_statuses_allowed_to(user).collect(&:id).include?(attrs['status_id'].to_i)
340 attrs.delete('status_id')
340 attrs.delete('status_id')
341 end
341 end
342 end
342 end
343
343
344 unless leaf?
344 unless leaf?
345 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
345 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
346 end
346 end
347
347
348 if attrs['parent_issue_id'].present?
348 if attrs['parent_issue_id'].present?
349 attrs.delete('parent_issue_id') unless Issue.visible(user).exists?(attrs['parent_issue_id'].to_i)
349 attrs.delete('parent_issue_id') unless Issue.visible(user).exists?(attrs['parent_issue_id'].to_i)
350 end
350 end
351
351
352 # mass-assignment security bypass
352 # mass-assignment security bypass
353 self.send :attributes=, attrs, false
353 self.send :attributes=, attrs, false
354 end
354 end
355
355
356 def done_ratio
356 def done_ratio
357 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
357 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
358 status.default_done_ratio
358 status.default_done_ratio
359 else
359 else
360 read_attribute(:done_ratio)
360 read_attribute(:done_ratio)
361 end
361 end
362 end
362 end
363
363
364 def self.use_status_for_done_ratio?
364 def self.use_status_for_done_ratio?
365 Setting.issue_done_ratio == 'issue_status'
365 Setting.issue_done_ratio == 'issue_status'
366 end
366 end
367
367
368 def self.use_field_for_done_ratio?
368 def self.use_field_for_done_ratio?
369 Setting.issue_done_ratio == 'issue_field'
369 Setting.issue_done_ratio == 'issue_field'
370 end
370 end
371
371
372 def validate_issue
372 def validate_issue
373 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
373 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
374 errors.add :due_date, :not_a_date
374 errors.add :due_date, :not_a_date
375 end
375 end
376
376
377 if self.due_date and self.start_date and self.due_date < self.start_date
377 if self.due_date and self.start_date and self.due_date < self.start_date
378 errors.add :due_date, :greater_than_start_date
378 errors.add :due_date, :greater_than_start_date
379 end
379 end
380
380
381 if start_date && soonest_start && start_date < soonest_start
381 if start_date && soonest_start && start_date < soonest_start
382 errors.add :start_date, :invalid
382 errors.add :start_date, :invalid
383 end
383 end
384
384
385 if fixed_version
385 if fixed_version
386 if !assignable_versions.include?(fixed_version)
386 if !assignable_versions.include?(fixed_version)
387 errors.add :fixed_version_id, :inclusion
387 errors.add :fixed_version_id, :inclusion
388 elsif reopened? && fixed_version.closed?
388 elsif reopened? && fixed_version.closed?
389 errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version)
389 errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version)
390 end
390 end
391 end
391 end
392
392
393 # Checks that the issue can not be added/moved to a disabled tracker
393 # Checks that the issue can not be added/moved to a disabled tracker
394 if project && (tracker_id_changed? || project_id_changed?)
394 if project && (tracker_id_changed? || project_id_changed?)
395 unless project.trackers.include?(tracker)
395 unless project.trackers.include?(tracker)
396 errors.add :tracker_id, :inclusion
396 errors.add :tracker_id, :inclusion
397 end
397 end
398 end
398 end
399
399
400 # Checks parent issue assignment
400 # Checks parent issue assignment
401 if @parent_issue
401 if @parent_issue
402 if @parent_issue.project_id != project_id
402 if @parent_issue.project_id != project_id
403 errors.add :parent_issue_id, :not_same_project
403 errors.add :parent_issue_id, :not_same_project
404 elsif !new_record?
404 elsif !new_record?
405 # moving an existing issue
405 # moving an existing issue
406 if @parent_issue.root_id != root_id
406 if @parent_issue.root_id != root_id
407 # we can always move to another tree
407 # we can always move to another tree
408 elsif move_possible?(@parent_issue)
408 elsif move_possible?(@parent_issue)
409 # move accepted inside tree
409 # move accepted inside tree
410 else
410 else
411 errors.add :parent_issue_id, :not_a_valid_parent
411 errors.add :parent_issue_id, :not_a_valid_parent
412 end
412 end
413 end
413 end
414 end
414 end
415 end
415 end
416
416
417 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
417 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
418 # even if the user turns off the setting later
418 # even if the user turns off the setting later
419 def update_done_ratio_from_issue_status
419 def update_done_ratio_from_issue_status
420 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
420 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
421 self.done_ratio = status.default_done_ratio
421 self.done_ratio = status.default_done_ratio
422 end
422 end
423 end
423 end
424
424
425 def init_journal(user, notes = "")
425 def init_journal(user, notes = "")
426 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
426 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
427 if new_record?
427 if new_record?
428 @current_journal.notify = false
428 @current_journal.notify = false
429 else
429 else
430 @attributes_before_change = attributes.dup
430 @attributes_before_change = attributes.dup
431 @custom_values_before_change = {}
431 @custom_values_before_change = {}
432 self.custom_field_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
432 self.custom_field_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
433 end
433 end
434 # Make sure updated_on is updated when adding a note.
434 # Make sure updated_on is updated when adding a note.
435 updated_on_will_change!
435 updated_on_will_change!
436 @current_journal
436 @current_journal
437 end
437 end
438
438
439 # Returns the id of the last journal or nil
439 # Returns the id of the last journal or nil
440 def last_journal_id
440 def last_journal_id
441 if new_record?
441 if new_record?
442 nil
442 nil
443 else
443 else
444 journals.first(:order => "#{Journal.table_name}.id DESC").try(:id)
444 journals.first(:order => "#{Journal.table_name}.id DESC").try(:id)
445 end
445 end
446 end
446 end
447
447
448 # Return true if the issue is closed, otherwise false
448 # Return true if the issue is closed, otherwise false
449 def closed?
449 def closed?
450 self.status.is_closed?
450 self.status.is_closed?
451 end
451 end
452
452
453 # Return true if the issue is being reopened
453 # Return true if the issue is being reopened
454 def reopened?
454 def reopened?
455 if !new_record? && status_id_changed?
455 if !new_record? && status_id_changed?
456 status_was = IssueStatus.find_by_id(status_id_was)
456 status_was = IssueStatus.find_by_id(status_id_was)
457 status_new = IssueStatus.find_by_id(status_id)
457 status_new = IssueStatus.find_by_id(status_id)
458 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
458 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
459 return true
459 return true
460 end
460 end
461 end
461 end
462 false
462 false
463 end
463 end
464
464
465 # Return true if the issue is being closed
465 # Return true if the issue is being closed
466 def closing?
466 def closing?
467 if !new_record? && status_id_changed?
467 if !new_record? && status_id_changed?
468 status_was = IssueStatus.find_by_id(status_id_was)
468 status_was = IssueStatus.find_by_id(status_id_was)
469 status_new = IssueStatus.find_by_id(status_id)
469 status_new = IssueStatus.find_by_id(status_id)
470 if status_was && status_new && !status_was.is_closed? && status_new.is_closed?
470 if status_was && status_new && !status_was.is_closed? && status_new.is_closed?
471 return true
471 return true
472 end
472 end
473 end
473 end
474 false
474 false
475 end
475 end
476
476
477 # Returns true if the issue is overdue
477 # Returns true if the issue is overdue
478 def overdue?
478 def overdue?
479 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
479 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
480 end
480 end
481
481
482 # Is the amount of work done less than it should for the due date
482 # Is the amount of work done less than it should for the due date
483 def behind_schedule?
483 def behind_schedule?
484 return false if start_date.nil? || due_date.nil?
484 return false if start_date.nil? || due_date.nil?
485 done_date = start_date + ((due_date - start_date+1)* done_ratio/100).floor
485 done_date = start_date + ((due_date - start_date+1)* done_ratio/100).floor
486 return done_date <= Date.today
486 return done_date <= Date.today
487 end
487 end
488
488
489 # Does this issue have children?
489 # Does this issue have children?
490 def children?
490 def children?
491 !leaf?
491 !leaf?
492 end
492 end
493
493
494 # Users the issue can be assigned to
494 # Users the issue can be assigned to
495 def assignable_users
495 def assignable_users
496 users = project.assignable_users
496 users = project.assignable_users
497 users << author if author
497 users << author if author
498 users << assigned_to if assigned_to
498 users << assigned_to if assigned_to
499 users.uniq.sort
499 users.uniq.sort
500 end
500 end
501
501
502 # Versions that the issue can be assigned to
502 # Versions that the issue can be assigned to
503 def assignable_versions
503 def assignable_versions
504 @assignable_versions ||= (project.shared_versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort
504 @assignable_versions ||= (project.shared_versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort
505 end
505 end
506
506
507 # Returns true if this issue is blocked by another issue that is still open
507 # Returns true if this issue is blocked by another issue that is still open
508 def blocked?
508 def blocked?
509 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
509 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
510 end
510 end
511
511
512 # Returns an array of status that user is able to apply
512 # Returns an array of status that user is able to apply
513 def new_statuses_allowed_to(user=User.current, include_default=false)
513 def new_statuses_allowed_to(user=User.current, include_default=false)
514 statuses = status.find_new_statuses_allowed_to(
514 statuses = status.find_new_statuses_allowed_to(
515 user.admin ? Role.all : user.roles_for_project(project),
515 user.admin ? Role.all : user.roles_for_project(project),
516 tracker,
516 tracker,
517 author == user,
517 author == user,
518 assigned_to_id_changed? ? assigned_to_id_was == user.id : assigned_to_id == user.id
518 assigned_to_id_changed? ? assigned_to_id_was == user.id : assigned_to_id == user.id
519 )
519 )
520 statuses << status unless statuses.empty?
520 statuses << status unless statuses.empty?
521 statuses << IssueStatus.default if include_default
521 statuses << IssueStatus.default if include_default
522 statuses = statuses.uniq.sort
522 statuses = statuses.uniq.sort
523 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
523 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
524 end
524 end
525
525
526 def assigned_to_was
526 def assigned_to_was
527 if assigned_to_id_changed? && assigned_to_id_was.present?
527 if assigned_to_id_changed? && assigned_to_id_was.present?
528 @assigned_to_was ||= User.find_by_id(assigned_to_id_was)
528 @assigned_to_was ||= User.find_by_id(assigned_to_id_was)
529 end
529 end
530 end
530 end
531
531
532 # Returns the mail adresses of users that should be notified
532 # Returns the mail adresses of users that should be notified
533 def recipients
533 def recipients
534 notified = []
534 notified = []
535 # Author and assignee are always notified unless they have been
535 # Author and assignee are always notified unless they have been
536 # locked or don't want to be notified
536 # locked or don't want to be notified
537 notified << author if author
537 notified << author if author
538 if assigned_to
538 if assigned_to
539 notified += (assigned_to.is_a?(Group) ? assigned_to.users : [assigned_to])
539 notified += (assigned_to.is_a?(Group) ? assigned_to.users : [assigned_to])
540 end
540 end
541 if assigned_to_was
541 if assigned_to_was
542 notified += (assigned_to_was.is_a?(Group) ? assigned_to_was.users : [assigned_to_was])
542 notified += (assigned_to_was.is_a?(Group) ? assigned_to_was.users : [assigned_to_was])
543 end
543 end
544 notified = notified.select {|u| u.active? && u.notify_about?(self)}
544 notified = notified.select {|u| u.active? && u.notify_about?(self)}
545
545
546 notified += project.notified_users
546 notified += project.notified_users
547 notified.uniq!
547 notified.uniq!
548 # Remove users that can not view the issue
548 # Remove users that can not view the issue
549 notified.reject! {|user| !visible?(user)}
549 notified.reject! {|user| !visible?(user)}
550 notified.collect(&:mail)
550 notified.collect(&:mail)
551 end
551 end
552
552
553 # Returns the number of hours spent on this issue
553 # Returns the number of hours spent on this issue
554 def spent_hours
554 def spent_hours
555 @spent_hours ||= time_entries.sum(:hours) || 0
555 @spent_hours ||= time_entries.sum(:hours) || 0
556 end
556 end
557
557
558 # Returns the total number of hours spent on this issue and its descendants
558 # Returns the total number of hours spent on this issue and its descendants
559 #
559 #
560 # Example:
560 # Example:
561 # spent_hours => 0.0
561 # spent_hours => 0.0
562 # spent_hours => 50.2
562 # spent_hours => 50.2
563 def total_spent_hours
563 def total_spent_hours
564 @total_spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours",
564 @total_spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours",
565 :joins => "LEFT JOIN #{TimeEntry.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id").to_f || 0.0
565 :joins => "LEFT JOIN #{TimeEntry.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id").to_f || 0.0
566 end
566 end
567
567
568 def relations
568 def relations
569 @relations ||= (relations_from + relations_to).sort
569 @relations ||= (relations_from + relations_to).sort
570 end
570 end
571
571
572 # Preloads relations for a collection of issues
572 # Preloads relations for a collection of issues
573 def self.load_relations(issues)
573 def self.load_relations(issues)
574 if issues.any?
574 if issues.any?
575 relations = IssueRelation.all(:conditions => ["issue_from_id IN (:ids) OR issue_to_id IN (:ids)", {:ids => issues.map(&:id)}])
575 relations = IssueRelation.all(:conditions => ["issue_from_id IN (:ids) OR issue_to_id IN (:ids)", {:ids => issues.map(&:id)}])
576 issues.each do |issue|
576 issues.each do |issue|
577 issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
577 issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
578 end
578 end
579 end
579 end
580 end
580 end
581
581
582 # Preloads visible spent time for a collection of issues
582 # Preloads visible spent time for a collection of issues
583 def self.load_visible_spent_hours(issues, user=User.current)
583 def self.load_visible_spent_hours(issues, user=User.current)
584 if issues.any?
584 if issues.any?
585 hours_by_issue_id = TimeEntry.visible(user).sum(:hours, :group => :issue_id)
585 hours_by_issue_id = TimeEntry.visible(user).sum(:hours, :group => :issue_id)
586 issues.each do |issue|
586 issues.each do |issue|
587 issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0)
587 issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0)
588 end
588 end
589 end
589 end
590 end
590 end
591
591
592 # Finds an issue relation given its id.
592 # Finds an issue relation given its id.
593 def find_relation(relation_id)
593 def find_relation(relation_id)
594 IssueRelation.find(relation_id, :conditions => ["issue_to_id = ? OR issue_from_id = ?", id, id])
594 IssueRelation.find(relation_id, :conditions => ["issue_to_id = ? OR issue_from_id = ?", id, id])
595 end
595 end
596
596
597 def all_dependent_issues(except=[])
597 def all_dependent_issues(except=[])
598 except << self
598 except << self
599 dependencies = []
599 dependencies = []
600 relations_from.each do |relation|
600 relations_from.each do |relation|
601 if relation.issue_to && !except.include?(relation.issue_to)
601 if relation.issue_to && !except.include?(relation.issue_to)
602 dependencies << relation.issue_to
602 dependencies << relation.issue_to
603 dependencies += relation.issue_to.all_dependent_issues(except)
603 dependencies += relation.issue_to.all_dependent_issues(except)
604 end
604 end
605 end
605 end
606 dependencies
606 dependencies
607 end
607 end
608
608
609 # Returns an array of issues that duplicate this one
609 # Returns an array of issues that duplicate this one
610 def duplicates
610 def duplicates
611 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
611 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
612 end
612 end
613
613
614 # Returns the due date or the target due date if any
614 # Returns the due date or the target due date if any
615 # Used on gantt chart
615 # Used on gantt chart
616 def due_before
616 def due_before
617 due_date || (fixed_version ? fixed_version.effective_date : nil)
617 due_date || (fixed_version ? fixed_version.effective_date : nil)
618 end
618 end
619
619
620 # Returns the time scheduled for this issue.
620 # Returns the time scheduled for this issue.
621 #
621 #
622 # Example:
622 # Example:
623 # Start Date: 2/26/09, End Date: 3/04/09
623 # Start Date: 2/26/09, End Date: 3/04/09
624 # duration => 6
624 # duration => 6
625 def duration
625 def duration
626 (start_date && due_date) ? due_date - start_date : 0
626 (start_date && due_date) ? due_date - start_date : 0
627 end
627 end
628
628
629 def soonest_start
629 def soonest_start
630 @soonest_start ||= (
630 @soonest_start ||= (
631 relations_to.collect{|relation| relation.successor_soonest_start} +
631 relations_to.collect{|relation| relation.successor_soonest_start} +
632 ancestors.collect(&:soonest_start)
632 ancestors.collect(&:soonest_start)
633 ).compact.max
633 ).compact.max
634 end
634 end
635
635
636 def reschedule_after(date)
636 def reschedule_after(date)
637 return if date.nil?
637 return if date.nil?
638 if leaf?
638 if leaf?
639 if start_date.nil? || start_date < date
639 if start_date.nil? || start_date < date
640 self.start_date, self.due_date = date, date + duration
640 self.start_date, self.due_date = date, date + duration
641 begin
641 begin
642 save
642 save
643 rescue ActiveRecord::StaleObjectError
643 rescue ActiveRecord::StaleObjectError
644 reload
644 reload
645 self.start_date, self.due_date = date, date + duration
645 self.start_date, self.due_date = date, date + duration
646 save
646 save
647 end
647 end
648 end
648 end
649 else
649 else
650 leaves.each do |leaf|
650 leaves.each do |leaf|
651 leaf.reschedule_after(date)
651 leaf.reschedule_after(date)
652 end
652 end
653 end
653 end
654 end
654 end
655
655
656 def <=>(issue)
656 def <=>(issue)
657 if issue.nil?
657 if issue.nil?
658 -1
658 -1
659 elsif root_id != issue.root_id
659 elsif root_id != issue.root_id
660 (root_id || 0) <=> (issue.root_id || 0)
660 (root_id || 0) <=> (issue.root_id || 0)
661 else
661 else
662 (lft || 0) <=> (issue.lft || 0)
662 (lft || 0) <=> (issue.lft || 0)
663 end
663 end
664 end
664 end
665
665
666 def to_s
666 def to_s
667 "#{tracker} ##{id}: #{subject}"
667 "#{tracker} ##{id}: #{subject}"
668 end
668 end
669
669
670 # Returns a string of css classes that apply to the issue
670 # Returns a string of css classes that apply to the issue
671 def css_classes
671 def css_classes
672 s = "issue status-#{status.position} priority-#{priority.position}"
672 s = "issue status-#{status.position} priority-#{priority.position}"
673 s << ' closed' if closed?
673 s << ' closed' if closed?
674 s << ' overdue' if overdue?
674 s << ' overdue' if overdue?
675 s << ' child' if child?
675 s << ' child' if child?
676 s << ' parent' unless leaf?
676 s << ' parent' unless leaf?
677 s << ' private' if is_private?
677 s << ' private' if is_private?
678 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
678 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
679 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
679 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
680 s
680 s
681 end
681 end
682
682
683 # Saves an issue, time_entry, attachments, and a journal from the parameters
683 # Saves an issue and a time_entry from the parameters
684 # Returns false if save fails
685 def save_issue_with_child_records(params, existing_time_entry=nil)
684 def save_issue_with_child_records(params, existing_time_entry=nil)
686 Issue.transaction do
685 Issue.transaction do
687 if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, project)
686 if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, project)
688 @time_entry = existing_time_entry || TimeEntry.new
687 @time_entry = existing_time_entry || TimeEntry.new
689 @time_entry.project = project
688 @time_entry.project = project
690 @time_entry.issue = self
689 @time_entry.issue = self
691 @time_entry.user = User.current
690 @time_entry.user = User.current
692 @time_entry.spent_on = User.current.today
691 @time_entry.spent_on = User.current.today
693 @time_entry.attributes = params[:time_entry]
692 @time_entry.attributes = params[:time_entry]
694 self.time_entries << @time_entry
693 self.time_entries << @time_entry
695 end
694 end
696
695
697 if valid?
696 # TODO: Rename hook
698 attachments = Attachment.attach_files(self, params[:attachments])
697 Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
698 if save
699 # TODO: Rename hook
699 # TODO: Rename hook
700 Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
700 Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
701 begin
701 else
702 if save
702 raise ActiveRecord::Rollback
703 # TODO: Rename hook
704 Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
705 else
706 raise ActiveRecord::Rollback
707 end
708 rescue ActiveRecord::StaleObjectError
709 attachments[:files].each(&:destroy)
710 raise ActiveRecord::StaleObjectError
711 end
712 end
703 end
713 end
704 end
714 end
705 end
715
706
716 # Unassigns issues from +version+ if it's no longer shared with issue's project
707 # Unassigns issues from +version+ if it's no longer shared with issue's project
717 def self.update_versions_from_sharing_change(version)
708 def self.update_versions_from_sharing_change(version)
718 # Update issues assigned to the version
709 # Update issues assigned to the version
719 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
710 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
720 end
711 end
721
712
722 # Unassigns issues from versions that are no longer shared
713 # Unassigns issues from versions that are no longer shared
723 # after +project+ was moved
714 # after +project+ was moved
724 def self.update_versions_from_hierarchy_change(project)
715 def self.update_versions_from_hierarchy_change(project)
725 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
716 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
726 # Update issues of the moved projects and issues assigned to a version of a moved project
717 # Update issues of the moved projects and issues assigned to a version of a moved project
727 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
718 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
728 end
719 end
729
720
730 def parent_issue_id=(arg)
721 def parent_issue_id=(arg)
731 parent_issue_id = arg.blank? ? nil : arg.to_i
722 parent_issue_id = arg.blank? ? nil : arg.to_i
732 if parent_issue_id && @parent_issue = Issue.find_by_id(parent_issue_id)
723 if parent_issue_id && @parent_issue = Issue.find_by_id(parent_issue_id)
733 @parent_issue.id
724 @parent_issue.id
734 else
725 else
735 @parent_issue = nil
726 @parent_issue = nil
736 nil
727 nil
737 end
728 end
738 end
729 end
739
730
740 def parent_issue_id
731 def parent_issue_id
741 if instance_variable_defined? :@parent_issue
732 if instance_variable_defined? :@parent_issue
742 @parent_issue.nil? ? nil : @parent_issue.id
733 @parent_issue.nil? ? nil : @parent_issue.id
743 else
734 else
744 parent_id
735 parent_id
745 end
736 end
746 end
737 end
747
738
748 # Extracted from the ReportsController.
739 # Extracted from the ReportsController.
749 def self.by_tracker(project)
740 def self.by_tracker(project)
750 count_and_group_by(:project => project,
741 count_and_group_by(:project => project,
751 :field => 'tracker_id',
742 :field => 'tracker_id',
752 :joins => Tracker.table_name)
743 :joins => Tracker.table_name)
753 end
744 end
754
745
755 def self.by_version(project)
746 def self.by_version(project)
756 count_and_group_by(:project => project,
747 count_and_group_by(:project => project,
757 :field => 'fixed_version_id',
748 :field => 'fixed_version_id',
758 :joins => Version.table_name)
749 :joins => Version.table_name)
759 end
750 end
760
751
761 def self.by_priority(project)
752 def self.by_priority(project)
762 count_and_group_by(:project => project,
753 count_and_group_by(:project => project,
763 :field => 'priority_id',
754 :field => 'priority_id',
764 :joins => IssuePriority.table_name)
755 :joins => IssuePriority.table_name)
765 end
756 end
766
757
767 def self.by_category(project)
758 def self.by_category(project)
768 count_and_group_by(:project => project,
759 count_and_group_by(:project => project,
769 :field => 'category_id',
760 :field => 'category_id',
770 :joins => IssueCategory.table_name)
761 :joins => IssueCategory.table_name)
771 end
762 end
772
763
773 def self.by_assigned_to(project)
764 def self.by_assigned_to(project)
774 count_and_group_by(:project => project,
765 count_and_group_by(:project => project,
775 :field => 'assigned_to_id',
766 :field => 'assigned_to_id',
776 :joins => User.table_name)
767 :joins => User.table_name)
777 end
768 end
778
769
779 def self.by_author(project)
770 def self.by_author(project)
780 count_and_group_by(:project => project,
771 count_and_group_by(:project => project,
781 :field => 'author_id',
772 :field => 'author_id',
782 :joins => User.table_name)
773 :joins => User.table_name)
783 end
774 end
784
775
785 def self.by_subproject(project)
776 def self.by_subproject(project)
786 ActiveRecord::Base.connection.select_all("select s.id as status_id,
777 ActiveRecord::Base.connection.select_all("select s.id as status_id,
787 s.is_closed as closed,
778 s.is_closed as closed,
788 #{Issue.table_name}.project_id as project_id,
779 #{Issue.table_name}.project_id as project_id,
789 count(#{Issue.table_name}.id) as total
780 count(#{Issue.table_name}.id) as total
790 from
781 from
791 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s
782 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s
792 where
783 where
793 #{Issue.table_name}.status_id=s.id
784 #{Issue.table_name}.status_id=s.id
794 and #{Issue.table_name}.project_id = #{Project.table_name}.id
785 and #{Issue.table_name}.project_id = #{Project.table_name}.id
795 and #{visible_condition(User.current, :project => project, :with_subprojects => true)}
786 and #{visible_condition(User.current, :project => project, :with_subprojects => true)}
796 and #{Issue.table_name}.project_id <> #{project.id}
787 and #{Issue.table_name}.project_id <> #{project.id}
797 group by s.id, s.is_closed, #{Issue.table_name}.project_id") if project.descendants.active.any?
788 group by s.id, s.is_closed, #{Issue.table_name}.project_id") if project.descendants.active.any?
798 end
789 end
799 # End ReportsController extraction
790 # End ReportsController extraction
800
791
801 # Returns an array of projects that user can assign the issue to
792 # Returns an array of projects that user can assign the issue to
802 def allowed_target_projects(user=User.current)
793 def allowed_target_projects(user=User.current)
803 if new_record?
794 if new_record?
804 Project.all(:conditions => Project.allowed_to_condition(user, :add_issues))
795 Project.all(:conditions => Project.allowed_to_condition(user, :add_issues))
805 else
796 else
806 self.class.allowed_target_projects_on_move(user)
797 self.class.allowed_target_projects_on_move(user)
807 end
798 end
808 end
799 end
809
800
810 # Returns an array of projects that user can move issues to
801 # Returns an array of projects that user can move issues to
811 def self.allowed_target_projects_on_move(user=User.current)
802 def self.allowed_target_projects_on_move(user=User.current)
812 projects = []
803 projects = []
813 if user.admin?
804 if user.admin?
814 # admin is allowed to move issues to any active (visible) project
805 # admin is allowed to move issues to any active (visible) project
815 projects = Project.visible(user).all
806 projects = Project.visible(user).all
816 elsif user.logged?
807 elsif user.logged?
817 if Role.non_member.allowed_to?(:move_issues)
808 if Role.non_member.allowed_to?(:move_issues)
818 projects = Project.visible(user).all
809 projects = Project.visible(user).all
819 else
810 else
820 user.memberships.each {|m| projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}}
811 user.memberships.each {|m| projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}}
821 end
812 end
822 end
813 end
823 projects
814 projects
824 end
815 end
825
816
826 private
817 private
827
818
828 def after_project_change
819 def after_project_change
829 # Update project_id on related time entries
820 # Update project_id on related time entries
830 TimeEntry.update_all(["project_id = ?", project_id], {:issue_id => id})
821 TimeEntry.update_all(["project_id = ?", project_id], {:issue_id => id})
831
822
832 # Delete issue relations
823 # Delete issue relations
833 unless Setting.cross_project_issue_relations?
824 unless Setting.cross_project_issue_relations?
834 relations_from.clear
825 relations_from.clear
835 relations_to.clear
826 relations_to.clear
836 end
827 end
837
828
838 # Move subtasks
829 # Move subtasks
839 children.each do |child|
830 children.each do |child|
840 # Change project and keep project
831 # Change project and keep project
841 child.send :project=, project, true
832 child.send :project=, project, true
842 unless child.save
833 unless child.save
843 raise ActiveRecord::Rollback
834 raise ActiveRecord::Rollback
844 end
835 end
845 end
836 end
846 end
837 end
847
838
848 def update_nested_set_attributes
839 def update_nested_set_attributes
849 if root_id.nil?
840 if root_id.nil?
850 # issue was just created
841 # issue was just created
851 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
842 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
852 set_default_left_and_right
843 set_default_left_and_right
853 Issue.update_all("root_id = #{root_id}, lft = #{lft}, rgt = #{rgt}", ["id = ?", id])
844 Issue.update_all("root_id = #{root_id}, lft = #{lft}, rgt = #{rgt}", ["id = ?", id])
854 if @parent_issue
845 if @parent_issue
855 move_to_child_of(@parent_issue)
846 move_to_child_of(@parent_issue)
856 end
847 end
857 reload
848 reload
858 elsif parent_issue_id != parent_id
849 elsif parent_issue_id != parent_id
859 former_parent_id = parent_id
850 former_parent_id = parent_id
860 # moving an existing issue
851 # moving an existing issue
861 if @parent_issue && @parent_issue.root_id == root_id
852 if @parent_issue && @parent_issue.root_id == root_id
862 # inside the same tree
853 # inside the same tree
863 move_to_child_of(@parent_issue)
854 move_to_child_of(@parent_issue)
864 else
855 else
865 # to another tree
856 # to another tree
866 unless root?
857 unless root?
867 move_to_right_of(root)
858 move_to_right_of(root)
868 reload
859 reload
869 end
860 end
870 old_root_id = root_id
861 old_root_id = root_id
871 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
862 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
872 target_maxright = nested_set_scope.maximum(right_column_name) || 0
863 target_maxright = nested_set_scope.maximum(right_column_name) || 0
873 offset = target_maxright + 1 - lft
864 offset = target_maxright + 1 - lft
874 Issue.update_all("root_id = #{root_id}, lft = lft + #{offset}, rgt = rgt + #{offset}",
865 Issue.update_all("root_id = #{root_id}, lft = lft + #{offset}, rgt = rgt + #{offset}",
875 ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
866 ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
876 self[left_column_name] = lft + offset
867 self[left_column_name] = lft + offset
877 self[right_column_name] = rgt + offset
868 self[right_column_name] = rgt + offset
878 if @parent_issue
869 if @parent_issue
879 move_to_child_of(@parent_issue)
870 move_to_child_of(@parent_issue)
880 end
871 end
881 end
872 end
882 reload
873 reload
883 # delete invalid relations of all descendants
874 # delete invalid relations of all descendants
884 self_and_descendants.each do |issue|
875 self_and_descendants.each do |issue|
885 issue.relations.each do |relation|
876 issue.relations.each do |relation|
886 relation.destroy unless relation.valid?
877 relation.destroy unless relation.valid?
887 end
878 end
888 end
879 end
889 # update former parent
880 # update former parent
890 recalculate_attributes_for(former_parent_id) if former_parent_id
881 recalculate_attributes_for(former_parent_id) if former_parent_id
891 end
882 end
892 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
883 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
893 end
884 end
894
885
895 def update_parent_attributes
886 def update_parent_attributes
896 recalculate_attributes_for(parent_id) if parent_id
887 recalculate_attributes_for(parent_id) if parent_id
897 end
888 end
898
889
899 def recalculate_attributes_for(issue_id)
890 def recalculate_attributes_for(issue_id)
900 if issue_id && p = Issue.find_by_id(issue_id)
891 if issue_id && p = Issue.find_by_id(issue_id)
901 # priority = highest priority of children
892 # priority = highest priority of children
902 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :joins => :priority)
893 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :joins => :priority)
903 p.priority = IssuePriority.find_by_position(priority_position)
894 p.priority = IssuePriority.find_by_position(priority_position)
904 end
895 end
905
896
906 # start/due dates = lowest/highest dates of children
897 # start/due dates = lowest/highest dates of children
907 p.start_date = p.children.minimum(:start_date)
898 p.start_date = p.children.minimum(:start_date)
908 p.due_date = p.children.maximum(:due_date)
899 p.due_date = p.children.maximum(:due_date)
909 if p.start_date && p.due_date && p.due_date < p.start_date
900 if p.start_date && p.due_date && p.due_date < p.start_date
910 p.start_date, p.due_date = p.due_date, p.start_date
901 p.start_date, p.due_date = p.due_date, p.start_date
911 end
902 end
912
903
913 # done ratio = weighted average ratio of leaves
904 # done ratio = weighted average ratio of leaves
914 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
905 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
915 leaves_count = p.leaves.count
906 leaves_count = p.leaves.count
916 if leaves_count > 0
907 if leaves_count > 0
917 average = p.leaves.average(:estimated_hours).to_f
908 average = p.leaves.average(:estimated_hours).to_f
918 if average == 0
909 if average == 0
919 average = 1
910 average = 1
920 end
911 end
921 done = p.leaves.sum("COALESCE(estimated_hours, #{average}) * (CASE WHEN is_closed = #{connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)", :joins => :status).to_f
912 done = p.leaves.sum("COALESCE(estimated_hours, #{average}) * (CASE WHEN is_closed = #{connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)", :joins => :status).to_f
922 progress = done / (average * leaves_count)
913 progress = done / (average * leaves_count)
923 p.done_ratio = progress.round
914 p.done_ratio = progress.round
924 end
915 end
925 end
916 end
926
917
927 # estimate = sum of leaves estimates
918 # estimate = sum of leaves estimates
928 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
919 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
929 p.estimated_hours = nil if p.estimated_hours == 0.0
920 p.estimated_hours = nil if p.estimated_hours == 0.0
930
921
931 # ancestors will be recursively updated
922 # ancestors will be recursively updated
932 p.save(false)
923 p.save(false)
933 end
924 end
934 end
925 end
935
926
936 # Update issues so their versions are not pointing to a
927 # Update issues so their versions are not pointing to a
937 # fixed_version that is not shared with the issue's project
928 # fixed_version that is not shared with the issue's project
938 def self.update_versions(conditions=nil)
929 def self.update_versions(conditions=nil)
939 # Only need to update issues with a fixed_version from
930 # Only need to update issues with a fixed_version from
940 # a different project and that is not systemwide shared
931 # a different project and that is not systemwide shared
941 Issue.scoped(:conditions => conditions).all(
932 Issue.scoped(:conditions => conditions).all(
942 :conditions => "#{Issue.table_name}.fixed_version_id IS NOT NULL" +
933 :conditions => "#{Issue.table_name}.fixed_version_id IS NOT NULL" +
943 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
934 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
944 " AND #{Version.table_name}.sharing <> 'system'",
935 " AND #{Version.table_name}.sharing <> 'system'",
945 :include => [:project, :fixed_version]
936 :include => [:project, :fixed_version]
946 ).each do |issue|
937 ).each do |issue|
947 next if issue.project.nil? || issue.fixed_version.nil?
938 next if issue.project.nil? || issue.fixed_version.nil?
948 unless issue.project.shared_versions.include?(issue.fixed_version)
939 unless issue.project.shared_versions.include?(issue.fixed_version)
949 issue.init_journal(User.current)
940 issue.init_journal(User.current)
950 issue.fixed_version = nil
941 issue.fixed_version = nil
951 issue.save
942 issue.save
952 end
943 end
953 end
944 end
954 end
945 end
955
946
956 # Callback on attachment deletion
947 # Callback on attachment deletion
957 def attachment_added(obj)
948 def attachment_added(obj)
958 if @current_journal && !obj.new_record?
949 if @current_journal && !obj.new_record?
959 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :value => obj.filename)
950 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :value => obj.filename)
960 end
951 end
961 end
952 end
962
953
963 # Callback on attachment deletion
954 # Callback on attachment deletion
964 def attachment_removed(obj)
955 def attachment_removed(obj)
965 journal = init_journal(User.current)
956 journal = init_journal(User.current)
966 journal.details << JournalDetail.new(:property => 'attachment',
957 journal.details << JournalDetail.new(:property => 'attachment',
967 :prop_key => obj.id,
958 :prop_key => obj.id,
968 :old_value => obj.filename)
959 :old_value => obj.filename)
969 journal.save
960 journal.save
970 end
961 end
971
962
972 # Default assignment based on category
963 # Default assignment based on category
973 def default_assign
964 def default_assign
974 if assigned_to.nil? && category && category.assigned_to
965 if assigned_to.nil? && category && category.assigned_to
975 self.assigned_to = category.assigned_to
966 self.assigned_to = category.assigned_to
976 end
967 end
977 end
968 end
978
969
979 # Updates start/due dates of following issues
970 # Updates start/due dates of following issues
980 def reschedule_following_issues
971 def reschedule_following_issues
981 if start_date_changed? || due_date_changed?
972 if start_date_changed? || due_date_changed?
982 relations_from.each do |relation|
973 relations_from.each do |relation|
983 relation.set_issue_to_dates
974 relation.set_issue_to_dates
984 end
975 end
985 end
976 end
986 end
977 end
987
978
988 # Closes duplicates if the issue is being closed
979 # Closes duplicates if the issue is being closed
989 def close_duplicates
980 def close_duplicates
990 if closing?
981 if closing?
991 duplicates.each do |duplicate|
982 duplicates.each do |duplicate|
992 # Reload is need in case the duplicate was updated by a previous duplicate
983 # Reload is need in case the duplicate was updated by a previous duplicate
993 duplicate.reload
984 duplicate.reload
994 # Don't re-close it if it's already closed
985 # Don't re-close it if it's already closed
995 next if duplicate.closed?
986 next if duplicate.closed?
996 # Same user and notes
987 # Same user and notes
997 if @current_journal
988 if @current_journal
998 duplicate.init_journal(@current_journal.user, @current_journal.notes)
989 duplicate.init_journal(@current_journal.user, @current_journal.notes)
999 end
990 end
1000 duplicate.update_attribute :status, self.status
991 duplicate.update_attribute :status, self.status
1001 end
992 end
1002 end
993 end
1003 end
994 end
1004
995
1005 # Saves the changes in a Journal
996 # Saves the changes in a Journal
1006 # Called after_save
997 # Called after_save
1007 def create_journal
998 def create_journal
1008 if @current_journal
999 if @current_journal
1009 # attributes changes
1000 # attributes changes
1010 if @attributes_before_change
1001 if @attributes_before_change
1011 (Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on)).each {|c|
1002 (Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on)).each {|c|
1012 before = @attributes_before_change[c]
1003 before = @attributes_before_change[c]
1013 after = send(c)
1004 after = send(c)
1014 next if before == after || (before.blank? && after.blank?)
1005 next if before == after || (before.blank? && after.blank?)
1015 @current_journal.details << JournalDetail.new(:property => 'attr',
1006 @current_journal.details << JournalDetail.new(:property => 'attr',
1016 :prop_key => c,
1007 :prop_key => c,
1017 :old_value => before,
1008 :old_value => before,
1018 :value => after)
1009 :value => after)
1019 }
1010 }
1020 end
1011 end
1021 if @custom_values_before_change
1012 if @custom_values_before_change
1022 # custom fields changes
1013 # custom fields changes
1023 custom_field_values.each {|c|
1014 custom_field_values.each {|c|
1024 before = @custom_values_before_change[c.custom_field_id]
1015 before = @custom_values_before_change[c.custom_field_id]
1025 after = c.value
1016 after = c.value
1026 next if before == after || (before.blank? && after.blank?)
1017 next if before == after || (before.blank? && after.blank?)
1027
1018
1028 if before.is_a?(Array) || after.is_a?(Array)
1019 if before.is_a?(Array) || after.is_a?(Array)
1029 before = [before] unless before.is_a?(Array)
1020 before = [before] unless before.is_a?(Array)
1030 after = [after] unless after.is_a?(Array)
1021 after = [after] unless after.is_a?(Array)
1031
1022
1032 # values removed
1023 # values removed
1033 (before - after).reject(&:blank?).each do |value|
1024 (before - after).reject(&:blank?).each do |value|
1034 @current_journal.details << JournalDetail.new(:property => 'cf',
1025 @current_journal.details << JournalDetail.new(:property => 'cf',
1035 :prop_key => c.custom_field_id,
1026 :prop_key => c.custom_field_id,
1036 :old_value => value,
1027 :old_value => value,
1037 :value => nil)
1028 :value => nil)
1038 end
1029 end
1039 # values added
1030 # values added
1040 (after - before).reject(&:blank?).each do |value|
1031 (after - before).reject(&:blank?).each do |value|
1041 @current_journal.details << JournalDetail.new(:property => 'cf',
1032 @current_journal.details << JournalDetail.new(:property => 'cf',
1042 :prop_key => c.custom_field_id,
1033 :prop_key => c.custom_field_id,
1043 :old_value => nil,
1034 :old_value => nil,
1044 :value => value)
1035 :value => value)
1045 end
1036 end
1046 else
1037 else
1047 @current_journal.details << JournalDetail.new(:property => 'cf',
1038 @current_journal.details << JournalDetail.new(:property => 'cf',
1048 :prop_key => c.custom_field_id,
1039 :prop_key => c.custom_field_id,
1049 :old_value => before,
1040 :old_value => before,
1050 :value => after)
1041 :value => after)
1051 end
1042 end
1052 }
1043 }
1053 end
1044 end
1054 @current_journal.save
1045 @current_journal.save
1055 # reset current journal
1046 # reset current journal
1056 init_journal @current_journal.user, @current_journal.notes
1047 init_journal @current_journal.user, @current_journal.notes
1057 end
1048 end
1058 end
1049 end
1059
1050
1060 # Query generator for selecting groups of issue counts for a project
1051 # Query generator for selecting groups of issue counts for a project
1061 # based on specific criteria
1052 # based on specific criteria
1062 #
1053 #
1063 # Options
1054 # Options
1064 # * project - Project to search in.
1055 # * project - Project to search in.
1065 # * field - String. Issue field to key off of in the grouping.
1056 # * field - String. Issue field to key off of in the grouping.
1066 # * joins - String. The table name to join against.
1057 # * joins - String. The table name to join against.
1067 def self.count_and_group_by(options)
1058 def self.count_and_group_by(options)
1068 project = options.delete(:project)
1059 project = options.delete(:project)
1069 select_field = options.delete(:field)
1060 select_field = options.delete(:field)
1070 joins = options.delete(:joins)
1061 joins = options.delete(:joins)
1071
1062
1072 where = "#{Issue.table_name}.#{select_field}=j.id"
1063 where = "#{Issue.table_name}.#{select_field}=j.id"
1073
1064
1074 ActiveRecord::Base.connection.select_all("select s.id as status_id,
1065 ActiveRecord::Base.connection.select_all("select s.id as status_id,
1075 s.is_closed as closed,
1066 s.is_closed as closed,
1076 j.id as #{select_field},
1067 j.id as #{select_field},
1077 count(#{Issue.table_name}.id) as total
1068 count(#{Issue.table_name}.id) as total
1078 from
1069 from
1079 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s, #{joins} j
1070 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s, #{joins} j
1080 where
1071 where
1081 #{Issue.table_name}.status_id=s.id
1072 #{Issue.table_name}.status_id=s.id
1082 and #{where}
1073 and #{where}
1083 and #{Issue.table_name}.project_id=#{Project.table_name}.id
1074 and #{Issue.table_name}.project_id=#{Project.table_name}.id
1084 and #{visible_condition(User.current, :project => project)}
1075 and #{visible_condition(User.current, :project => project)}
1085 group by s.id, s.is_closed, j.id")
1076 group by s.id, s.is_closed, j.id")
1086 end
1077 end
1087 end
1078 end
@@ -1,11 +1,19
1 <% if defined?(container) && container && container.saved_attachments %>
2 <% container.saved_attachments.each_with_index do |attachment, i| %>
3 <span class="icon icon-attachment" style="display:block; line-height:1.5em;">
4 <%= h(attachment.filename) %> (<%= number_to_human_size(attachment.filesize) %>)
5 <%= hidden_field_tag "attachments[p#{i}][token]", "#{attachment.id}.#{attachment.digest}" %>
6 </span>
7 <% end %>
8 <% end %>
1 <span id="attachments_fields">
9 <span id="attachments_fields">
2 <span>
10 <span>
3 <%= file_field_tag 'attachments[1][file]', :size => 30, :id => nil, :class => 'file',
11 <%= file_field_tag 'attachments[1][file]', :size => 30, :id => nil, :class => 'file',
4 :onchange => "checkFileSize(this, #{Setting.attachment_max_size.to_i.kilobytes}, '#{escape_javascript(l(:error_attachment_too_big, :max_size => number_to_human_size(Setting.attachment_max_size.to_i.kilobytes)))}');" -%>
12 :onchange => "checkFileSize(this, #{Setting.attachment_max_size.to_i.kilobytes}, '#{escape_javascript(l(:error_attachment_too_big, :max_size => number_to_human_size(Setting.attachment_max_size.to_i.kilobytes)))}');" -%>
5 <%= text_field_tag 'attachments[1][description]', '', :id => nil, :class => 'description', :placeholder => l(:label_optional_description) %>
13 <%= text_field_tag 'attachments[1][description]', '', :id => nil, :class => 'description', :placeholder => l(:label_optional_description) %>
6 <%= link_to_function(image_tag('delete.png'), 'removeFileField(this)', :title => (l(:button_delete))) %>
14 <%= link_to_function(image_tag('delete.png'), 'removeFileField(this)', :title => (l(:button_delete))) %>
7 </span>
15 </span>
8 </span>
16 </span>
9 <small><%= link_to l(:label_add_another_file), '#', :onclick => 'addFileField(); return false;' %>
17 <small><%= link_to l(:label_add_another_file), '#', :onclick => 'addFileField(); return false;' %>
10 (<%= l(:label_max_size) %>: <%= number_to_human_size(Setting.attachment_max_size.to_i.kilobytes) %>)
18 (<%= l(:label_max_size) %>: <%= number_to_human_size(Setting.attachment_max_size.to_i.kilobytes) %>)
11 </small>
19 </small>
@@ -1,50 +1,50
1 <% labelled_form_for @issue, :html => {:id => 'issue-form', :multipart => true} do |f| %>
1 <% labelled_form_for @issue, :html => {:id => 'issue-form', :multipart => true} do |f| %>
2 <%= error_messages_for 'issue', 'time_entry' %>
2 <%= error_messages_for 'issue', 'time_entry' %>
3 <%= render :partial => 'conflict' if @conflict %>
3 <%= render :partial => 'conflict' if @conflict %>
4 <div class="box">
4 <div class="box">
5 <% if @edit_allowed || !@allowed_statuses.empty? %>
5 <% if @edit_allowed || !@allowed_statuses.empty? %>
6 <fieldset class="tabular"><legend><%= l(:label_change_properties) %></legend>
6 <fieldset class="tabular"><legend><%= l(:label_change_properties) %></legend>
7 <div id="all_attributes">
7 <div id="all_attributes">
8 <%= render :partial => 'form', :locals => {:f => f} %>
8 <%= render :partial => 'form', :locals => {:f => f} %>
9 </div>
9 </div>
10 </fieldset>
10 </fieldset>
11 <% end %>
11 <% end %>
12 <% if User.current.allowed_to?(:log_time, @project) %>
12 <% if User.current.allowed_to?(:log_time, @project) %>
13 <fieldset class="tabular"><legend><%= l(:button_log_time) %></legend>
13 <fieldset class="tabular"><legend><%= l(:button_log_time) %></legend>
14 <% labelled_fields_for :time_entry, @time_entry do |time_entry| %>
14 <% labelled_fields_for :time_entry, @time_entry do |time_entry| %>
15 <div class="splitcontentleft">
15 <div class="splitcontentleft">
16 <p><%= time_entry.text_field :hours, :size => 6, :label => :label_spent_time %> <%= l(:field_hours) %></p>
16 <p><%= time_entry.text_field :hours, :size => 6, :label => :label_spent_time %> <%= l(:field_hours) %></p>
17 </div>
17 </div>
18 <div class="splitcontentright">
18 <div class="splitcontentright">
19 <p><%= time_entry.select :activity_id, activity_collection_for_select_options %></p>
19 <p><%= time_entry.select :activity_id, activity_collection_for_select_options %></p>
20 </div>
20 </div>
21 <p><%= time_entry.text_field :comments, :size => 60 %></p>
21 <p><%= time_entry.text_field :comments, :size => 60 %></p>
22 <% @time_entry.custom_field_values.each do |value| %>
22 <% @time_entry.custom_field_values.each do |value| %>
23 <p><%= custom_field_tag_with_label :time_entry, value %></p>
23 <p><%= custom_field_tag_with_label :time_entry, value %></p>
24 <% end %>
24 <% end %>
25 <% end %>
25 <% end %>
26 </fieldset>
26 </fieldset>
27 <% end %>
27 <% end %>
28
28
29 <fieldset><legend><%= l(:field_notes) %></legend>
29 <fieldset><legend><%= l(:field_notes) %></legend>
30 <%= text_area_tag 'notes', @notes, :cols => 60, :rows => 10, :class => 'wiki-edit' %>
30 <%= text_area_tag 'notes', @notes, :cols => 60, :rows => 10, :class => 'wiki-edit' %>
31 <%= wikitoolbar_for 'notes' %>
31 <%= wikitoolbar_for 'notes' %>
32 <%= call_hook(:view_issues_edit_notes_bottom, { :issue => @issue, :notes => @notes, :form => f }) %>
32 <%= call_hook(:view_issues_edit_notes_bottom, { :issue => @issue, :notes => @notes, :form => f }) %>
33
33
34 <p><%=l(:label_attachment_plural)%><br /><%= render :partial => 'attachments/form' %></p>
34 <p><%=l(:label_attachment_plural)%><br /><%= render :partial => 'attachments/form', :locals => {:container => @issue} %></p>
35 </fieldset>
35 </fieldset>
36 </div>
36 </div>
37
37
38 <%= f.hidden_field :lock_version %>
38 <%= f.hidden_field :lock_version %>
39 <%= hidden_field_tag 'last_journal_id', params[:last_journal_id] || @issue.last_journal_id %>
39 <%= hidden_field_tag 'last_journal_id', params[:last_journal_id] || @issue.last_journal_id %>
40 <%= submit_tag l(:button_submit) %>
40 <%= submit_tag l(:button_submit) %>
41 <%= link_to_remote l(:label_preview),
41 <%= link_to_remote l(:label_preview),
42 { :url => preview_edit_issue_path(:project_id => @project, :id => @issue),
42 { :url => preview_edit_issue_path(:project_id => @project, :id => @issue),
43 :method => 'post',
43 :method => 'post',
44 :update => 'preview',
44 :update => 'preview',
45 :with => 'Form.serialize("issue-form")',
45 :with => 'Form.serialize("issue-form")',
46 :complete => "Element.scrollTo('preview')"
46 :complete => "Element.scrollTo('preview')"
47 }, :accesskey => accesskey(:preview) %>
47 }, :accesskey => accesskey(:preview) %>
48 <% end %>
48 <% end %>
49
49
50 <div id="preview" class="wiki"></div>
50 <div id="preview" class="wiki"></div>
@@ -1,50 +1,50
1 <h2><%=l(:label_issue_new)%></h2>
1 <h2><%=l(:label_issue_new)%></h2>
2
2
3 <%= call_hook(:view_issues_new_top, {:issue => @issue}) %>
3 <%= call_hook(:view_issues_new_top, {:issue => @issue}) %>
4
4
5 <% labelled_form_for @issue, :url => project_issues_path(@project),
5 <% labelled_form_for @issue, :url => project_issues_path(@project),
6 :html => {:id => 'issue-form', :multipart => true} do |f| %>
6 :html => {:id => 'issue-form', :multipart => true} do |f| %>
7 <%= error_messages_for 'issue' %>
7 <%= error_messages_for 'issue' %>
8 <%= hidden_field_tag 'copy_from', params[:copy_from] if params[:copy_from] %>
8 <%= hidden_field_tag 'copy_from', params[:copy_from] if params[:copy_from] %>
9 <div class="box tabular">
9 <div class="box tabular">
10 <div id="all_attributes">
10 <div id="all_attributes">
11 <%= render :partial => 'issues/form', :locals => {:f => f} %>
11 <%= render :partial => 'issues/form', :locals => {:f => f} %>
12 </div>
12 </div>
13
13
14 <% if @copy_from && @copy_from.attachments.any? %>
14 <% if @copy_from && @copy_from.attachments.any? %>
15 <p>
15 <p>
16 <label for="copy_attachments"><%= l(:label_copy_attachments) %></label>
16 <label for="copy_attachments"><%= l(:label_copy_attachments) %></label>
17 <%= check_box_tag 'copy_attachments', '1', @copy_attachments %>
17 <%= check_box_tag 'copy_attachments', '1', @copy_attachments %>
18 </p>
18 </p>
19 <% end %>
19 <% end %>
20
20
21 <p id="attachments_form"><%= label_tag('attachments[1][file]', l(:label_attachment_plural))%><%= render :partial => 'attachments/form' %></p>
21 <p id="attachments_form"><%= label_tag('attachments[1][file]', l(:label_attachment_plural))%><%= render :partial => 'attachments/form', :locals => {:container => @issue} %></p>
22
22
23 <% if @issue.safe_attribute? 'watcher_user_ids' -%>
23 <% if @issue.safe_attribute? 'watcher_user_ids' -%>
24 <p id="watchers_form"><label><%= l(:label_issue_watchers) %></label>
24 <p id="watchers_form"><label><%= l(:label_issue_watchers) %></label>
25 <% @issue.project.users.sort.each do |user| -%>
25 <% @issue.project.users.sort.each do |user| -%>
26 <label class="floating"><%= check_box_tag 'issue[watcher_user_ids][]', user.id, @issue.watched_by?(user), :id => nil %> <%=h user %></label>
26 <label class="floating"><%= check_box_tag 'issue[watcher_user_ids][]', user.id, @issue.watched_by?(user), :id => nil %> <%=h user %></label>
27 <% end -%>
27 <% end -%>
28 </p>
28 </p>
29 <% end %>
29 <% end %>
30 </div>
30 </div>
31
31
32 <%= submit_tag l(:button_create) %>
32 <%= submit_tag l(:button_create) %>
33 <%= submit_tag l(:button_create_and_continue), :name => 'continue' %>
33 <%= submit_tag l(:button_create_and_continue), :name => 'continue' %>
34 <%= link_to_remote l(:label_preview),
34 <%= link_to_remote l(:label_preview),
35 { :url => preview_new_issue_path(:project_id => @project),
35 { :url => preview_new_issue_path(:project_id => @project),
36 :method => 'post',
36 :method => 'post',
37 :update => 'preview',
37 :update => 'preview',
38 :with => "Form.serialize('issue-form')",
38 :with => "Form.serialize('issue-form')",
39 :complete => "Element.scrollTo('preview')"
39 :complete => "Element.scrollTo('preview')"
40 }, :accesskey => accesskey(:preview) %>
40 }, :accesskey => accesskey(:preview) %>
41
41
42 <%= javascript_tag "Form.Element.focus('issue_subject');" %>
42 <%= javascript_tag "Form.Element.focus('issue_subject');" %>
43 <% end %>
43 <% end %>
44
44
45 <div id="preview" class="wiki"></div>
45 <div id="preview" class="wiki"></div>
46
46
47 <% content_for :header_tags do %>
47 <% content_for :header_tags do %>
48 <%= stylesheet_link_tag 'scm' %>
48 <%= stylesheet_link_tag 'scm' %>
49 <%= robot_exclusion_tag %>
49 <%= robot_exclusion_tag %>
50 <% end %>
50 <% end %>
@@ -1,299 +1,307
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2011 Jean-Philippe Lang
4 # Copyright (C) 2006-2011 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 File.expand_path('../../test_helper', __FILE__)
20 require File.expand_path('../../test_helper', __FILE__)
21 require 'attachments_controller'
21 require 'attachments_controller'
22
22
23 # Re-raise errors caught by the controller.
23 # Re-raise errors caught by the controller.
24 class AttachmentsController; def rescue_action(e) raise e end; end
24 class AttachmentsController; def rescue_action(e) raise e end; end
25
25
26 class AttachmentsControllerTest < ActionController::TestCase
26 class AttachmentsControllerTest < ActionController::TestCase
27 fixtures :users, :projects, :roles, :members, :member_roles,
27 fixtures :users, :projects, :roles, :members, :member_roles,
28 :enabled_modules, :issues, :trackers, :attachments,
28 :enabled_modules, :issues, :trackers, :attachments,
29 :versions, :wiki_pages, :wikis, :documents
29 :versions, :wiki_pages, :wikis, :documents
30
30
31 def setup
31 def setup
32 @controller = AttachmentsController.new
32 @controller = AttachmentsController.new
33 @request = ActionController::TestRequest.new
33 @request = ActionController::TestRequest.new
34 @response = ActionController::TestResponse.new
34 @response = ActionController::TestResponse.new
35 User.current = nil
35 User.current = nil
36 set_fixtures_attachments_directory
36 set_fixtures_attachments_directory
37 end
37 end
38
38
39 def teardown
39 def teardown
40 set_tmp_attachments_directory
40 set_tmp_attachments_directory
41 end
41 end
42
42
43 def test_show_diff
43 def test_show_diff
44 ['inline', 'sbs'].each do |dt|
44 ['inline', 'sbs'].each do |dt|
45 # 060719210727_changeset_utf8.diff
45 # 060719210727_changeset_utf8.diff
46 get :show, :id => 14, :type => dt
46 get :show, :id => 14, :type => dt
47 assert_response :success
47 assert_response :success
48 assert_template 'diff'
48 assert_template 'diff'
49 assert_equal 'text/html', @response.content_type
49 assert_equal 'text/html', @response.content_type
50 assert_tag 'th',
50 assert_tag 'th',
51 :attributes => {:class => /filename/},
51 :attributes => {:class => /filename/},
52 :content => /issues_controller.rb\t\(rΓ©vision 1484\)/
52 :content => /issues_controller.rb\t\(rΓ©vision 1484\)/
53 assert_tag 'td',
53 assert_tag 'td',
54 :attributes => {:class => /line-code/},
54 :attributes => {:class => /line-code/},
55 :content => /Demande créée avec succès/
55 :content => /Demande créée avec succès/
56 end
56 end
57 set_tmp_attachments_directory
57 set_tmp_attachments_directory
58 end
58 end
59
59
60 def test_show_diff_replcace_cannot_convert_content
60 def test_show_diff_replcace_cannot_convert_content
61 with_settings :repositories_encodings => 'UTF-8' do
61 with_settings :repositories_encodings => 'UTF-8' do
62 ['inline', 'sbs'].each do |dt|
62 ['inline', 'sbs'].each do |dt|
63 # 060719210727_changeset_iso8859-1.diff
63 # 060719210727_changeset_iso8859-1.diff
64 get :show, :id => 5, :type => dt
64 get :show, :id => 5, :type => dt
65 assert_response :success
65 assert_response :success
66 assert_template 'diff'
66 assert_template 'diff'
67 assert_equal 'text/html', @response.content_type
67 assert_equal 'text/html', @response.content_type
68 assert_tag 'th',
68 assert_tag 'th',
69 :attributes => {:class => "filename"},
69 :attributes => {:class => "filename"},
70 :content => /issues_controller.rb\t\(r\?vision 1484\)/
70 :content => /issues_controller.rb\t\(r\?vision 1484\)/
71 assert_tag 'td',
71 assert_tag 'td',
72 :attributes => {:class => /line-code/},
72 :attributes => {:class => /line-code/},
73 :content => /Demande cr\?\?e avec succ\?s/
73 :content => /Demande cr\?\?e avec succ\?s/
74 end
74 end
75 end
75 end
76 set_tmp_attachments_directory
76 set_tmp_attachments_directory
77 end
77 end
78
78
79 def test_show_diff_latin_1
79 def test_show_diff_latin_1
80 with_settings :repositories_encodings => 'UTF-8,ISO-8859-1' do
80 with_settings :repositories_encodings => 'UTF-8,ISO-8859-1' do
81 ['inline', 'sbs'].each do |dt|
81 ['inline', 'sbs'].each do |dt|
82 # 060719210727_changeset_iso8859-1.diff
82 # 060719210727_changeset_iso8859-1.diff
83 get :show, :id => 5, :type => dt
83 get :show, :id => 5, :type => dt
84 assert_response :success
84 assert_response :success
85 assert_template 'diff'
85 assert_template 'diff'
86 assert_equal 'text/html', @response.content_type
86 assert_equal 'text/html', @response.content_type
87 assert_tag 'th',
87 assert_tag 'th',
88 :attributes => {:class => "filename"},
88 :attributes => {:class => "filename"},
89 :content => /issues_controller.rb\t\(rΓ©vision 1484\)/
89 :content => /issues_controller.rb\t\(rΓ©vision 1484\)/
90 assert_tag 'td',
90 assert_tag 'td',
91 :attributes => {:class => /line-code/},
91 :attributes => {:class => /line-code/},
92 :content => /Demande créée avec succès/
92 :content => /Demande créée avec succès/
93 end
93 end
94 end
94 end
95 set_tmp_attachments_directory
95 set_tmp_attachments_directory
96 end
96 end
97
97
98 def test_save_diff_type
98 def test_save_diff_type
99 @request.session[:user_id] = 1 # admin
99 @request.session[:user_id] = 1 # admin
100 user = User.find(1)
100 user = User.find(1)
101 get :show, :id => 5
101 get :show, :id => 5
102 assert_response :success
102 assert_response :success
103 assert_template 'diff'
103 assert_template 'diff'
104 user.reload
104 user.reload
105 assert_equal "inline", user.pref[:diff_type]
105 assert_equal "inline", user.pref[:diff_type]
106 get :show, :id => 5, :type => 'sbs'
106 get :show, :id => 5, :type => 'sbs'
107 assert_response :success
107 assert_response :success
108 assert_template 'diff'
108 assert_template 'diff'
109 user.reload
109 user.reload
110 assert_equal "sbs", user.pref[:diff_type]
110 assert_equal "sbs", user.pref[:diff_type]
111 end
111 end
112
112
113 def test_show_text_file
113 def test_show_text_file
114 get :show, :id => 4
114 get :show, :id => 4
115 assert_response :success
115 assert_response :success
116 assert_template 'file'
116 assert_template 'file'
117 assert_equal 'text/html', @response.content_type
117 assert_equal 'text/html', @response.content_type
118 set_tmp_attachments_directory
118 set_tmp_attachments_directory
119 end
119 end
120
120
121 def test_show_text_file_utf_8
121 def test_show_text_file_utf_8
122 set_tmp_attachments_directory
122 set_tmp_attachments_directory
123 a = Attachment.new(:container => Issue.find(1),
123 a = Attachment.new(:container => Issue.find(1),
124 :file => uploaded_test_file("japanese-utf-8.txt", "text/plain"),
124 :file => uploaded_test_file("japanese-utf-8.txt", "text/plain"),
125 :author => User.find(1))
125 :author => User.find(1))
126 assert a.save
126 assert a.save
127 assert_equal 'japanese-utf-8.txt', a.filename
127 assert_equal 'japanese-utf-8.txt', a.filename
128
128
129 str_japanese = "\xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e"
129 str_japanese = "\xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e"
130 str_japanese.force_encoding('UTF-8') if str_japanese.respond_to?(:force_encoding)
130 str_japanese.force_encoding('UTF-8') if str_japanese.respond_to?(:force_encoding)
131
131
132 get :show, :id => a.id
132 get :show, :id => a.id
133 assert_response :success
133 assert_response :success
134 assert_template 'file'
134 assert_template 'file'
135 assert_equal 'text/html', @response.content_type
135 assert_equal 'text/html', @response.content_type
136 assert_tag :tag => 'th',
136 assert_tag :tag => 'th',
137 :content => '1',
137 :content => '1',
138 :attributes => { :class => 'line-num' },
138 :attributes => { :class => 'line-num' },
139 :sibling => { :tag => 'td', :content => /#{str_japanese}/ }
139 :sibling => { :tag => 'td', :content => /#{str_japanese}/ }
140 end
140 end
141
141
142 def test_show_text_file_replcace_cannot_convert_content
142 def test_show_text_file_replcace_cannot_convert_content
143 set_tmp_attachments_directory
143 set_tmp_attachments_directory
144 with_settings :repositories_encodings => 'UTF-8' do
144 with_settings :repositories_encodings => 'UTF-8' do
145 a = Attachment.new(:container => Issue.find(1),
145 a = Attachment.new(:container => Issue.find(1),
146 :file => uploaded_test_file("iso8859-1.txt", "text/plain"),
146 :file => uploaded_test_file("iso8859-1.txt", "text/plain"),
147 :author => User.find(1))
147 :author => User.find(1))
148 assert a.save
148 assert a.save
149 assert_equal 'iso8859-1.txt', a.filename
149 assert_equal 'iso8859-1.txt', a.filename
150
150
151 get :show, :id => a.id
151 get :show, :id => a.id
152 assert_response :success
152 assert_response :success
153 assert_template 'file'
153 assert_template 'file'
154 assert_equal 'text/html', @response.content_type
154 assert_equal 'text/html', @response.content_type
155 assert_tag :tag => 'th',
155 assert_tag :tag => 'th',
156 :content => '7',
156 :content => '7',
157 :attributes => { :class => 'line-num' },
157 :attributes => { :class => 'line-num' },
158 :sibling => { :tag => 'td', :content => /Demande cr\?\?e avec succ\?s/ }
158 :sibling => { :tag => 'td', :content => /Demande cr\?\?e avec succ\?s/ }
159 end
159 end
160 end
160 end
161
161
162 def test_show_text_file_latin_1
162 def test_show_text_file_latin_1
163 set_tmp_attachments_directory
163 set_tmp_attachments_directory
164 with_settings :repositories_encodings => 'UTF-8,ISO-8859-1' do
164 with_settings :repositories_encodings => 'UTF-8,ISO-8859-1' do
165 a = Attachment.new(:container => Issue.find(1),
165 a = Attachment.new(:container => Issue.find(1),
166 :file => uploaded_test_file("iso8859-1.txt", "text/plain"),
166 :file => uploaded_test_file("iso8859-1.txt", "text/plain"),
167 :author => User.find(1))
167 :author => User.find(1))
168 assert a.save
168 assert a.save
169 assert_equal 'iso8859-1.txt', a.filename
169 assert_equal 'iso8859-1.txt', a.filename
170
170
171 get :show, :id => a.id
171 get :show, :id => a.id
172 assert_response :success
172 assert_response :success
173 assert_template 'file'
173 assert_template 'file'
174 assert_equal 'text/html', @response.content_type
174 assert_equal 'text/html', @response.content_type
175 assert_tag :tag => 'th',
175 assert_tag :tag => 'th',
176 :content => '7',
176 :content => '7',
177 :attributes => { :class => 'line-num' },
177 :attributes => { :class => 'line-num' },
178 :sibling => { :tag => 'td', :content => /Demande créée avec succès/ }
178 :sibling => { :tag => 'td', :content => /Demande créée avec succès/ }
179 end
179 end
180 end
180 end
181
181
182 def test_show_text_file_should_send_if_too_big
182 def test_show_text_file_should_send_if_too_big
183 Setting.file_max_size_displayed = 512
183 Setting.file_max_size_displayed = 512
184 Attachment.find(4).update_attribute :filesize, 754.kilobyte
184 Attachment.find(4).update_attribute :filesize, 754.kilobyte
185
185
186 get :show, :id => 4
186 get :show, :id => 4
187 assert_response :success
187 assert_response :success
188 assert_equal 'application/x-ruby', @response.content_type
188 assert_equal 'application/x-ruby', @response.content_type
189 set_tmp_attachments_directory
189 set_tmp_attachments_directory
190 end
190 end
191
191
192 def test_show_other
192 def test_show_other
193 get :show, :id => 6
193 get :show, :id => 6
194 assert_response :success
194 assert_response :success
195 assert_equal 'application/octet-stream', @response.content_type
195 assert_equal 'application/octet-stream', @response.content_type
196 set_tmp_attachments_directory
196 set_tmp_attachments_directory
197 end
197 end
198
198
199 def test_show_file_from_private_issue_without_permission
199 def test_show_file_from_private_issue_without_permission
200 get :show, :id => 15
200 get :show, :id => 15
201 assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Fattachments%2F15'
201 assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Fattachments%2F15'
202 set_tmp_attachments_directory
202 set_tmp_attachments_directory
203 end
203 end
204
204
205 def test_show_file_from_private_issue_with_permission
205 def test_show_file_from_private_issue_with_permission
206 @request.session[:user_id] = 2
206 @request.session[:user_id] = 2
207 get :show, :id => 15
207 get :show, :id => 15
208 assert_response :success
208 assert_response :success
209 assert_tag 'h2', :content => /private.diff/
209 assert_tag 'h2', :content => /private.diff/
210 set_tmp_attachments_directory
210 set_tmp_attachments_directory
211 end
211 end
212
212
213 def test_show_file_without_container_should_be_denied
214 attachment = Attachment.create!(:file => uploaded_test_file("testfile.txt", "text/plain"), :author_id => 2)
215
216 @request.session[:user_id] = 2
217 get :show, :id => attachment.id
218 assert_response 403
219 end
220
213 def test_download_text_file
221 def test_download_text_file
214 get :download, :id => 4
222 get :download, :id => 4
215 assert_response :success
223 assert_response :success
216 assert_equal 'application/x-ruby', @response.content_type
224 assert_equal 'application/x-ruby', @response.content_type
217 set_tmp_attachments_directory
225 set_tmp_attachments_directory
218 end
226 end
219
227
220 def test_download_version_file_with_issue_tracking_disabled
228 def test_download_version_file_with_issue_tracking_disabled
221 Project.find(1).disable_module! :issue_tracking
229 Project.find(1).disable_module! :issue_tracking
222 get :download, :id => 9
230 get :download, :id => 9
223 assert_response :success
231 assert_response :success
224 end
232 end
225
233
226 def test_download_should_assign_content_type_if_blank
234 def test_download_should_assign_content_type_if_blank
227 Attachment.find(4).update_attribute(:content_type, '')
235 Attachment.find(4).update_attribute(:content_type, '')
228
236
229 get :download, :id => 4
237 get :download, :id => 4
230 assert_response :success
238 assert_response :success
231 assert_equal 'text/x-ruby', @response.content_type
239 assert_equal 'text/x-ruby', @response.content_type
232 set_tmp_attachments_directory
240 set_tmp_attachments_directory
233 end
241 end
234
242
235 def test_download_missing_file
243 def test_download_missing_file
236 get :download, :id => 2
244 get :download, :id => 2
237 assert_response 404
245 assert_response 404
238 set_tmp_attachments_directory
246 set_tmp_attachments_directory
239 end
247 end
240
248
241 def test_anonymous_on_private_private
249 def test_anonymous_on_private_private
242 get :download, :id => 7
250 get :download, :id => 7
243 assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Fattachments%2Fdownload%2F7'
251 assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Fattachments%2Fdownload%2F7'
244 set_tmp_attachments_directory
252 set_tmp_attachments_directory
245 end
253 end
246
254
247 def test_destroy_issue_attachment
255 def test_destroy_issue_attachment
248 set_tmp_attachments_directory
256 set_tmp_attachments_directory
249 issue = Issue.find(3)
257 issue = Issue.find(3)
250 @request.session[:user_id] = 2
258 @request.session[:user_id] = 2
251
259
252 assert_difference 'issue.attachments.count', -1 do
260 assert_difference 'issue.attachments.count', -1 do
253 delete :destroy, :id => 1
261 delete :destroy, :id => 1
254 end
262 end
255 # no referrer
263 # no referrer
256 assert_redirected_to '/projects/ecookbook'
264 assert_redirected_to '/projects/ecookbook'
257 assert_nil Attachment.find_by_id(1)
265 assert_nil Attachment.find_by_id(1)
258 j = issue.journals.find(:first, :order => 'created_on DESC')
266 j = issue.journals.find(:first, :order => 'created_on DESC')
259 assert_equal 'attachment', j.details.first.property
267 assert_equal 'attachment', j.details.first.property
260 assert_equal '1', j.details.first.prop_key
268 assert_equal '1', j.details.first.prop_key
261 assert_equal 'error281.txt', j.details.first.old_value
269 assert_equal 'error281.txt', j.details.first.old_value
262 end
270 end
263
271
264 def test_destroy_wiki_page_attachment
272 def test_destroy_wiki_page_attachment
265 set_tmp_attachments_directory
273 set_tmp_attachments_directory
266 @request.session[:user_id] = 2
274 @request.session[:user_id] = 2
267 assert_difference 'Attachment.count', -1 do
275 assert_difference 'Attachment.count', -1 do
268 delete :destroy, :id => 3
276 delete :destroy, :id => 3
269 assert_response 302
277 assert_response 302
270 end
278 end
271 end
279 end
272
280
273 def test_destroy_project_attachment
281 def test_destroy_project_attachment
274 set_tmp_attachments_directory
282 set_tmp_attachments_directory
275 @request.session[:user_id] = 2
283 @request.session[:user_id] = 2
276 assert_difference 'Attachment.count', -1 do
284 assert_difference 'Attachment.count', -1 do
277 delete :destroy, :id => 8
285 delete :destroy, :id => 8
278 assert_response 302
286 assert_response 302
279 end
287 end
280 end
288 end
281
289
282 def test_destroy_version_attachment
290 def test_destroy_version_attachment
283 set_tmp_attachments_directory
291 set_tmp_attachments_directory
284 @request.session[:user_id] = 2
292 @request.session[:user_id] = 2
285 assert_difference 'Attachment.count', -1 do
293 assert_difference 'Attachment.count', -1 do
286 delete :destroy, :id => 9
294 delete :destroy, :id => 9
287 assert_response 302
295 assert_response 302
288 end
296 end
289 end
297 end
290
298
291 def test_destroy_without_permission
299 def test_destroy_without_permission
292 set_tmp_attachments_directory
300 set_tmp_attachments_directory
293 assert_no_difference 'Attachment.count' do
301 assert_no_difference 'Attachment.count' do
294 delete :destroy, :id => 3
302 delete :destroy, :id => 3
295 end
303 end
296 assert_response 302
304 assert_response 302
297 assert Attachment.find_by_id(3)
305 assert Attachment.find_by_id(3)
298 end
306 end
299 end
307 end
@@ -1,2958 +1,3087
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 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.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19 require 'issues_controller'
19 require 'issues_controller'
20
20
21 class IssuesControllerTest < ActionController::TestCase
21 class IssuesControllerTest < ActionController::TestCase
22 fixtures :projects,
22 fixtures :projects,
23 :users,
23 :users,
24 :roles,
24 :roles,
25 :members,
25 :members,
26 :member_roles,
26 :member_roles,
27 :issues,
27 :issues,
28 :issue_statuses,
28 :issue_statuses,
29 :versions,
29 :versions,
30 :trackers,
30 :trackers,
31 :projects_trackers,
31 :projects_trackers,
32 :issue_categories,
32 :issue_categories,
33 :enabled_modules,
33 :enabled_modules,
34 :enumerations,
34 :enumerations,
35 :attachments,
35 :attachments,
36 :workflows,
36 :workflows,
37 :custom_fields,
37 :custom_fields,
38 :custom_values,
38 :custom_values,
39 :custom_fields_projects,
39 :custom_fields_projects,
40 :custom_fields_trackers,
40 :custom_fields_trackers,
41 :time_entries,
41 :time_entries,
42 :journals,
42 :journals,
43 :journal_details,
43 :journal_details,
44 :queries,
44 :queries,
45 :repositories,
45 :repositories,
46 :changesets
46 :changesets
47
47
48 include Redmine::I18n
48 include Redmine::I18n
49
49
50 def setup
50 def setup
51 @controller = IssuesController.new
51 @controller = IssuesController.new
52 @request = ActionController::TestRequest.new
52 @request = ActionController::TestRequest.new
53 @response = ActionController::TestResponse.new
53 @response = ActionController::TestResponse.new
54 User.current = nil
54 User.current = nil
55 end
55 end
56
56
57 def test_index
57 def test_index
58 with_settings :default_language => "en" do
58 with_settings :default_language => "en" do
59 get :index
59 get :index
60 assert_response :success
60 assert_response :success
61 assert_template 'index'
61 assert_template 'index'
62 assert_not_nil assigns(:issues)
62 assert_not_nil assigns(:issues)
63 assert_nil assigns(:project)
63 assert_nil assigns(:project)
64 assert_tag :tag => 'a', :content => /Can't print recipes/
64 assert_tag :tag => 'a', :content => /Can't print recipes/
65 assert_tag :tag => 'a', :content => /Subproject issue/
65 assert_tag :tag => 'a', :content => /Subproject issue/
66 # private projects hidden
66 # private projects hidden
67 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
67 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
68 assert_no_tag :tag => 'a', :content => /Issue on project 2/
68 assert_no_tag :tag => 'a', :content => /Issue on project 2/
69 # project column
69 # project column
70 assert_tag :tag => 'th', :content => /Project/
70 assert_tag :tag => 'th', :content => /Project/
71 end
71 end
72 end
72 end
73
73
74 def test_index_should_not_list_issues_when_module_disabled
74 def test_index_should_not_list_issues_when_module_disabled
75 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
75 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
76 get :index
76 get :index
77 assert_response :success
77 assert_response :success
78 assert_template 'index'
78 assert_template 'index'
79 assert_not_nil assigns(:issues)
79 assert_not_nil assigns(:issues)
80 assert_nil assigns(:project)
80 assert_nil assigns(:project)
81 assert_no_tag :tag => 'a', :content => /Can't print recipes/
81 assert_no_tag :tag => 'a', :content => /Can't print recipes/
82 assert_tag :tag => 'a', :content => /Subproject issue/
82 assert_tag :tag => 'a', :content => /Subproject issue/
83 end
83 end
84
84
85 def test_index_should_list_visible_issues_only
85 def test_index_should_list_visible_issues_only
86 get :index, :per_page => 100
86 get :index, :per_page => 100
87 assert_response :success
87 assert_response :success
88 assert_not_nil assigns(:issues)
88 assert_not_nil assigns(:issues)
89 assert_nil assigns(:issues).detect {|issue| !issue.visible?}
89 assert_nil assigns(:issues).detect {|issue| !issue.visible?}
90 end
90 end
91
91
92 def test_index_with_project
92 def test_index_with_project
93 Setting.display_subprojects_issues = 0
93 Setting.display_subprojects_issues = 0
94 get :index, :project_id => 1
94 get :index, :project_id => 1
95 assert_response :success
95 assert_response :success
96 assert_template 'index'
96 assert_template 'index'
97 assert_not_nil assigns(:issues)
97 assert_not_nil assigns(:issues)
98 assert_tag :tag => 'a', :content => /Can't print recipes/
98 assert_tag :tag => 'a', :content => /Can't print recipes/
99 assert_no_tag :tag => 'a', :content => /Subproject issue/
99 assert_no_tag :tag => 'a', :content => /Subproject issue/
100 end
100 end
101
101
102 def test_index_with_project_and_subprojects
102 def test_index_with_project_and_subprojects
103 Setting.display_subprojects_issues = 1
103 Setting.display_subprojects_issues = 1
104 get :index, :project_id => 1
104 get :index, :project_id => 1
105 assert_response :success
105 assert_response :success
106 assert_template 'index'
106 assert_template 'index'
107 assert_not_nil assigns(:issues)
107 assert_not_nil assigns(:issues)
108 assert_tag :tag => 'a', :content => /Can't print recipes/
108 assert_tag :tag => 'a', :content => /Can't print recipes/
109 assert_tag :tag => 'a', :content => /Subproject issue/
109 assert_tag :tag => 'a', :content => /Subproject issue/
110 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
110 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
111 end
111 end
112
112
113 def test_index_with_project_and_subprojects_should_show_private_subprojects
113 def test_index_with_project_and_subprojects_should_show_private_subprojects
114 @request.session[:user_id] = 2
114 @request.session[:user_id] = 2
115 Setting.display_subprojects_issues = 1
115 Setting.display_subprojects_issues = 1
116 get :index, :project_id => 1
116 get :index, :project_id => 1
117 assert_response :success
117 assert_response :success
118 assert_template 'index'
118 assert_template 'index'
119 assert_not_nil assigns(:issues)
119 assert_not_nil assigns(:issues)
120 assert_tag :tag => 'a', :content => /Can't print recipes/
120 assert_tag :tag => 'a', :content => /Can't print recipes/
121 assert_tag :tag => 'a', :content => /Subproject issue/
121 assert_tag :tag => 'a', :content => /Subproject issue/
122 assert_tag :tag => 'a', :content => /Issue of a private subproject/
122 assert_tag :tag => 'a', :content => /Issue of a private subproject/
123 end
123 end
124
124
125 def test_index_with_project_and_default_filter
125 def test_index_with_project_and_default_filter
126 get :index, :project_id => 1, :set_filter => 1
126 get :index, :project_id => 1, :set_filter => 1
127 assert_response :success
127 assert_response :success
128 assert_template 'index'
128 assert_template 'index'
129 assert_not_nil assigns(:issues)
129 assert_not_nil assigns(:issues)
130
130
131 query = assigns(:query)
131 query = assigns(:query)
132 assert_not_nil query
132 assert_not_nil query
133 # default filter
133 # default filter
134 assert_equal({'status_id' => {:operator => 'o', :values => ['']}}, query.filters)
134 assert_equal({'status_id' => {:operator => 'o', :values => ['']}}, query.filters)
135 end
135 end
136
136
137 def test_index_with_project_and_filter
137 def test_index_with_project_and_filter
138 get :index, :project_id => 1, :set_filter => 1,
138 get :index, :project_id => 1, :set_filter => 1,
139 :f => ['tracker_id'],
139 :f => ['tracker_id'],
140 :op => {'tracker_id' => '='},
140 :op => {'tracker_id' => '='},
141 :v => {'tracker_id' => ['1']}
141 :v => {'tracker_id' => ['1']}
142 assert_response :success
142 assert_response :success
143 assert_template 'index'
143 assert_template 'index'
144 assert_not_nil assigns(:issues)
144 assert_not_nil assigns(:issues)
145
145
146 query = assigns(:query)
146 query = assigns(:query)
147 assert_not_nil query
147 assert_not_nil query
148 assert_equal({'tracker_id' => {:operator => '=', :values => ['1']}}, query.filters)
148 assert_equal({'tracker_id' => {:operator => '=', :values => ['1']}}, query.filters)
149 end
149 end
150
150
151 def test_index_with_short_filters
151 def test_index_with_short_filters
152
152
153 to_test = {
153 to_test = {
154 'status_id' => {
154 'status_id' => {
155 'o' => { :op => 'o', :values => [''] },
155 'o' => { :op => 'o', :values => [''] },
156 'c' => { :op => 'c', :values => [''] },
156 'c' => { :op => 'c', :values => [''] },
157 '7' => { :op => '=', :values => ['7'] },
157 '7' => { :op => '=', :values => ['7'] },
158 '7|3|4' => { :op => '=', :values => ['7', '3', '4'] },
158 '7|3|4' => { :op => '=', :values => ['7', '3', '4'] },
159 '=7' => { :op => '=', :values => ['7'] },
159 '=7' => { :op => '=', :values => ['7'] },
160 '!3' => { :op => '!', :values => ['3'] },
160 '!3' => { :op => '!', :values => ['3'] },
161 '!7|3|4' => { :op => '!', :values => ['7', '3', '4'] }},
161 '!7|3|4' => { :op => '!', :values => ['7', '3', '4'] }},
162 'subject' => {
162 'subject' => {
163 'This is a subject' => { :op => '=', :values => ['This is a subject'] },
163 'This is a subject' => { :op => '=', :values => ['This is a subject'] },
164 'o' => { :op => '=', :values => ['o'] },
164 'o' => { :op => '=', :values => ['o'] },
165 '~This is part of a subject' => { :op => '~', :values => ['This is part of a subject'] },
165 '~This is part of a subject' => { :op => '~', :values => ['This is part of a subject'] },
166 '!~This is part of a subject' => { :op => '!~', :values => ['This is part of a subject'] }},
166 '!~This is part of a subject' => { :op => '!~', :values => ['This is part of a subject'] }},
167 'tracker_id' => {
167 'tracker_id' => {
168 '3' => { :op => '=', :values => ['3'] },
168 '3' => { :op => '=', :values => ['3'] },
169 '=3' => { :op => '=', :values => ['3'] }},
169 '=3' => { :op => '=', :values => ['3'] }},
170 'start_date' => {
170 'start_date' => {
171 '2011-10-12' => { :op => '=', :values => ['2011-10-12'] },
171 '2011-10-12' => { :op => '=', :values => ['2011-10-12'] },
172 '=2011-10-12' => { :op => '=', :values => ['2011-10-12'] },
172 '=2011-10-12' => { :op => '=', :values => ['2011-10-12'] },
173 '>=2011-10-12' => { :op => '>=', :values => ['2011-10-12'] },
173 '>=2011-10-12' => { :op => '>=', :values => ['2011-10-12'] },
174 '<=2011-10-12' => { :op => '<=', :values => ['2011-10-12'] },
174 '<=2011-10-12' => { :op => '<=', :values => ['2011-10-12'] },
175 '><2011-10-01|2011-10-30' => { :op => '><', :values => ['2011-10-01', '2011-10-30'] },
175 '><2011-10-01|2011-10-30' => { :op => '><', :values => ['2011-10-01', '2011-10-30'] },
176 '<t+2' => { :op => '<t+', :values => ['2'] },
176 '<t+2' => { :op => '<t+', :values => ['2'] },
177 '>t+2' => { :op => '>t+', :values => ['2'] },
177 '>t+2' => { :op => '>t+', :values => ['2'] },
178 't+2' => { :op => 't+', :values => ['2'] },
178 't+2' => { :op => 't+', :values => ['2'] },
179 't' => { :op => 't', :values => [''] },
179 't' => { :op => 't', :values => [''] },
180 'w' => { :op => 'w', :values => [''] },
180 'w' => { :op => 'w', :values => [''] },
181 '>t-2' => { :op => '>t-', :values => ['2'] },
181 '>t-2' => { :op => '>t-', :values => ['2'] },
182 '<t-2' => { :op => '<t-', :values => ['2'] },
182 '<t-2' => { :op => '<t-', :values => ['2'] },
183 't-2' => { :op => 't-', :values => ['2'] }},
183 't-2' => { :op => 't-', :values => ['2'] }},
184 'created_on' => {
184 'created_on' => {
185 '>=2011-10-12' => { :op => '>=', :values => ['2011-10-12'] },
185 '>=2011-10-12' => { :op => '>=', :values => ['2011-10-12'] },
186 '<t+2' => { :op => '=', :values => ['<t+2'] },
186 '<t+2' => { :op => '=', :values => ['<t+2'] },
187 '>t+2' => { :op => '=', :values => ['>t+2'] },
187 '>t+2' => { :op => '=', :values => ['>t+2'] },
188 't+2' => { :op => 't', :values => ['+2'] }},
188 't+2' => { :op => 't', :values => ['+2'] }},
189 'cf_1' => {
189 'cf_1' => {
190 'c' => { :op => '=', :values => ['c'] },
190 'c' => { :op => '=', :values => ['c'] },
191 '!c' => { :op => '!', :values => ['c'] },
191 '!c' => { :op => '!', :values => ['c'] },
192 '!*' => { :op => '!*', :values => [''] },
192 '!*' => { :op => '!*', :values => [''] },
193 '*' => { :op => '*', :values => [''] }},
193 '*' => { :op => '*', :values => [''] }},
194 'estimated_hours' => {
194 'estimated_hours' => {
195 '=13.4' => { :op => '=', :values => ['13.4'] },
195 '=13.4' => { :op => '=', :values => ['13.4'] },
196 '>=45' => { :op => '>=', :values => ['45'] },
196 '>=45' => { :op => '>=', :values => ['45'] },
197 '<=125' => { :op => '<=', :values => ['125'] },
197 '<=125' => { :op => '<=', :values => ['125'] },
198 '><10.5|20.5' => { :op => '><', :values => ['10.5', '20.5'] },
198 '><10.5|20.5' => { :op => '><', :values => ['10.5', '20.5'] },
199 '!*' => { :op => '!*', :values => [''] },
199 '!*' => { :op => '!*', :values => [''] },
200 '*' => { :op => '*', :values => [''] }}
200 '*' => { :op => '*', :values => [''] }}
201 }
201 }
202
202
203 default_filter = { 'status_id' => {:operator => 'o', :values => [''] }}
203 default_filter = { 'status_id' => {:operator => 'o', :values => [''] }}
204
204
205 to_test.each do |field, expression_and_expected|
205 to_test.each do |field, expression_and_expected|
206 expression_and_expected.each do |filter_expression, expected|
206 expression_and_expected.each do |filter_expression, expected|
207
207
208 get :index, :set_filter => 1, field => filter_expression
208 get :index, :set_filter => 1, field => filter_expression
209
209
210 assert_response :success
210 assert_response :success
211 assert_template 'index'
211 assert_template 'index'
212 assert_not_nil assigns(:issues)
212 assert_not_nil assigns(:issues)
213
213
214 query = assigns(:query)
214 query = assigns(:query)
215 assert_not_nil query
215 assert_not_nil query
216 assert query.has_filter?(field)
216 assert query.has_filter?(field)
217 assert_equal(default_filter.merge({field => {:operator => expected[:op], :values => expected[:values]}}), query.filters)
217 assert_equal(default_filter.merge({field => {:operator => expected[:op], :values => expected[:values]}}), query.filters)
218 end
218 end
219 end
219 end
220
220
221 end
221 end
222
222
223 def test_index_with_project_and_empty_filters
223 def test_index_with_project_and_empty_filters
224 get :index, :project_id => 1, :set_filter => 1, :fields => ['']
224 get :index, :project_id => 1, :set_filter => 1, :fields => ['']
225 assert_response :success
225 assert_response :success
226 assert_template 'index'
226 assert_template 'index'
227 assert_not_nil assigns(:issues)
227 assert_not_nil assigns(:issues)
228
228
229 query = assigns(:query)
229 query = assigns(:query)
230 assert_not_nil query
230 assert_not_nil query
231 # no filter
231 # no filter
232 assert_equal({}, query.filters)
232 assert_equal({}, query.filters)
233 end
233 end
234
234
235 def test_index_with_query
235 def test_index_with_query
236 get :index, :project_id => 1, :query_id => 5
236 get :index, :project_id => 1, :query_id => 5
237 assert_response :success
237 assert_response :success
238 assert_template 'index'
238 assert_template 'index'
239 assert_not_nil assigns(:issues)
239 assert_not_nil assigns(:issues)
240 assert_nil assigns(:issue_count_by_group)
240 assert_nil assigns(:issue_count_by_group)
241 end
241 end
242
242
243 def test_index_with_query_grouped_by_tracker
243 def test_index_with_query_grouped_by_tracker
244 get :index, :project_id => 1, :query_id => 6
244 get :index, :project_id => 1, :query_id => 6
245 assert_response :success
245 assert_response :success
246 assert_template 'index'
246 assert_template 'index'
247 assert_not_nil assigns(:issues)
247 assert_not_nil assigns(:issues)
248 assert_not_nil assigns(:issue_count_by_group)
248 assert_not_nil assigns(:issue_count_by_group)
249 end
249 end
250
250
251 def test_index_with_query_grouped_by_list_custom_field
251 def test_index_with_query_grouped_by_list_custom_field
252 get :index, :project_id => 1, :query_id => 9
252 get :index, :project_id => 1, :query_id => 9
253 assert_response :success
253 assert_response :success
254 assert_template 'index'
254 assert_template 'index'
255 assert_not_nil assigns(:issues)
255 assert_not_nil assigns(:issues)
256 assert_not_nil assigns(:issue_count_by_group)
256 assert_not_nil assigns(:issue_count_by_group)
257 end
257 end
258
258
259 def test_index_with_query_id_and_project_id_should_set_session_query
259 def test_index_with_query_id_and_project_id_should_set_session_query
260 get :index, :project_id => 1, :query_id => 4
260 get :index, :project_id => 1, :query_id => 4
261 assert_response :success
261 assert_response :success
262 assert_kind_of Hash, session[:query]
262 assert_kind_of Hash, session[:query]
263 assert_equal 4, session[:query][:id]
263 assert_equal 4, session[:query][:id]
264 assert_equal 1, session[:query][:project_id]
264 assert_equal 1, session[:query][:project_id]
265 end
265 end
266
266
267 def test_index_with_cross_project_query_in_session_should_show_project_issues
267 def test_index_with_cross_project_query_in_session_should_show_project_issues
268 q = Query.create!(:name => "test", :user_id => 2, :is_public => false, :project => nil)
268 q = Query.create!(:name => "test", :user_id => 2, :is_public => false, :project => nil)
269 @request.session[:query] = {:id => q.id, :project_id => 1}
269 @request.session[:query] = {:id => q.id, :project_id => 1}
270
270
271 with_settings :display_subprojects_issues => '0' do
271 with_settings :display_subprojects_issues => '0' do
272 get :index, :project_id => 1
272 get :index, :project_id => 1
273 end
273 end
274 assert_response :success
274 assert_response :success
275 assert_not_nil assigns(:query)
275 assert_not_nil assigns(:query)
276 assert_equal q.id, assigns(:query).id
276 assert_equal q.id, assigns(:query).id
277 assert_equal 1, assigns(:query).project_id
277 assert_equal 1, assigns(:query).project_id
278 assert_equal [1], assigns(:issues).map(&:project_id).uniq
278 assert_equal [1], assigns(:issues).map(&:project_id).uniq
279 end
279 end
280
280
281 def test_private_query_should_not_be_available_to_other_users
281 def test_private_query_should_not_be_available_to_other_users
282 q = Query.create!(:name => "private", :user => User.find(2), :is_public => false, :project => nil)
282 q = Query.create!(:name => "private", :user => User.find(2), :is_public => false, :project => nil)
283 @request.session[:user_id] = 3
283 @request.session[:user_id] = 3
284
284
285 get :index, :query_id => q.id
285 get :index, :query_id => q.id
286 assert_response 403
286 assert_response 403
287 end
287 end
288
288
289 def test_private_query_should_be_available_to_its_user
289 def test_private_query_should_be_available_to_its_user
290 q = Query.create!(:name => "private", :user => User.find(2), :is_public => false, :project => nil)
290 q = Query.create!(:name => "private", :user => User.find(2), :is_public => false, :project => nil)
291 @request.session[:user_id] = 2
291 @request.session[:user_id] = 2
292
292
293 get :index, :query_id => q.id
293 get :index, :query_id => q.id
294 assert_response :success
294 assert_response :success
295 end
295 end
296
296
297 def test_public_query_should_be_available_to_other_users
297 def test_public_query_should_be_available_to_other_users
298 q = Query.create!(:name => "private", :user => User.find(2), :is_public => true, :project => nil)
298 q = Query.create!(:name => "private", :user => User.find(2), :is_public => true, :project => nil)
299 @request.session[:user_id] = 3
299 @request.session[:user_id] = 3
300
300
301 get :index, :query_id => q.id
301 get :index, :query_id => q.id
302 assert_response :success
302 assert_response :success
303 end
303 end
304
304
305 def test_index_csv
305 def test_index_csv
306 get :index, :format => 'csv'
306 get :index, :format => 'csv'
307 assert_response :success
307 assert_response :success
308 assert_not_nil assigns(:issues)
308 assert_not_nil assigns(:issues)
309 assert_equal 'text/csv', @response.content_type
309 assert_equal 'text/csv', @response.content_type
310 assert @response.body.starts_with?("#,")
310 assert @response.body.starts_with?("#,")
311 lines = @response.body.chomp.split("\n")
311 lines = @response.body.chomp.split("\n")
312 assert_equal assigns(:query).columns.size + 1, lines[0].split(',').size
312 assert_equal assigns(:query).columns.size + 1, lines[0].split(',').size
313 end
313 end
314
314
315 def test_index_csv_with_project
315 def test_index_csv_with_project
316 get :index, :project_id => 1, :format => 'csv'
316 get :index, :project_id => 1, :format => 'csv'
317 assert_response :success
317 assert_response :success
318 assert_not_nil assigns(:issues)
318 assert_not_nil assigns(:issues)
319 assert_equal 'text/csv', @response.content_type
319 assert_equal 'text/csv', @response.content_type
320 end
320 end
321
321
322 def test_index_csv_with_description
322 def test_index_csv_with_description
323 get :index, :format => 'csv', :description => '1'
323 get :index, :format => 'csv', :description => '1'
324 assert_response :success
324 assert_response :success
325 assert_not_nil assigns(:issues)
325 assert_not_nil assigns(:issues)
326 assert_equal 'text/csv', @response.content_type
326 assert_equal 'text/csv', @response.content_type
327 assert @response.body.starts_with?("#,")
327 assert @response.body.starts_with?("#,")
328 lines = @response.body.chomp.split("\n")
328 lines = @response.body.chomp.split("\n")
329 assert_equal assigns(:query).columns.size + 2, lines[0].split(',').size
329 assert_equal assigns(:query).columns.size + 2, lines[0].split(',').size
330 end
330 end
331
331
332 def test_index_csv_with_spent_time_column
332 def test_index_csv_with_spent_time_column
333 issue = Issue.generate!(:project_id => 1, :tracker_id => 1, :subject => 'test_index_csv_with_spent_time_column')
333 issue = Issue.generate!(:project_id => 1, :tracker_id => 1, :subject => 'test_index_csv_with_spent_time_column')
334 TimeEntry.generate!(:project_id => issue.project_id, :issue_id => issue.id, :hours => 7.33)
334 TimeEntry.generate!(:project_id => issue.project_id, :issue_id => issue.id, :hours => 7.33)
335
335
336 get :index, :format => 'csv', :set_filter => '1', :c => %w(subject spent_hours)
336 get :index, :format => 'csv', :set_filter => '1', :c => %w(subject spent_hours)
337 assert_response :success
337 assert_response :success
338 assert_equal 'text/csv', @response.content_type
338 assert_equal 'text/csv', @response.content_type
339 lines = @response.body.chomp.split("\n")
339 lines = @response.body.chomp.split("\n")
340 assert_include "#{issue.id},#{issue.subject},7.33", lines
340 assert_include "#{issue.id},#{issue.subject},7.33", lines
341 end
341 end
342
342
343 def test_index_csv_with_all_columns
343 def test_index_csv_with_all_columns
344 get :index, :format => 'csv', :columns => 'all'
344 get :index, :format => 'csv', :columns => 'all'
345 assert_response :success
345 assert_response :success
346 assert_not_nil assigns(:issues)
346 assert_not_nil assigns(:issues)
347 assert_equal 'text/csv', @response.content_type
347 assert_equal 'text/csv', @response.content_type
348 assert @response.body.starts_with?("#,")
348 assert @response.body.starts_with?("#,")
349 lines = @response.body.chomp.split("\n")
349 lines = @response.body.chomp.split("\n")
350 assert_equal assigns(:query).available_columns.size + 1, lines[0].split(',').size
350 assert_equal assigns(:query).available_columns.size + 1, lines[0].split(',').size
351 end
351 end
352
352
353 def test_index_csv_with_multi_column_field
353 def test_index_csv_with_multi_column_field
354 CustomField.find(1).update_attribute :multiple, true
354 CustomField.find(1).update_attribute :multiple, true
355 issue = Issue.find(1)
355 issue = Issue.find(1)
356 issue.custom_field_values = {1 => ['MySQL', 'Oracle']}
356 issue.custom_field_values = {1 => ['MySQL', 'Oracle']}
357 issue.save!
357 issue.save!
358
358
359 get :index, :format => 'csv', :columns => 'all'
359 get :index, :format => 'csv', :columns => 'all'
360 assert_response :success
360 assert_response :success
361 lines = @response.body.chomp.split("\n")
361 lines = @response.body.chomp.split("\n")
362 assert lines.detect {|line| line.include?('"MySQL, Oracle"')}
362 assert lines.detect {|line| line.include?('"MySQL, Oracle"')}
363 end
363 end
364
364
365 def test_index_csv_big_5
365 def test_index_csv_big_5
366 with_settings :default_language => "zh-TW" do
366 with_settings :default_language => "zh-TW" do
367 str_utf8 = "\xe4\xb8\x80\xe6\x9c\x88"
367 str_utf8 = "\xe4\xb8\x80\xe6\x9c\x88"
368 str_big5 = "\xa4@\xa4\xeb"
368 str_big5 = "\xa4@\xa4\xeb"
369 if str_utf8.respond_to?(:force_encoding)
369 if str_utf8.respond_to?(:force_encoding)
370 str_utf8.force_encoding('UTF-8')
370 str_utf8.force_encoding('UTF-8')
371 str_big5.force_encoding('Big5')
371 str_big5.force_encoding('Big5')
372 end
372 end
373 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
373 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
374 :status_id => 1, :priority => IssuePriority.all.first,
374 :status_id => 1, :priority => IssuePriority.all.first,
375 :subject => str_utf8)
375 :subject => str_utf8)
376 assert issue.save
376 assert issue.save
377
377
378 get :index, :project_id => 1,
378 get :index, :project_id => 1,
379 :f => ['subject'],
379 :f => ['subject'],
380 :op => '=', :values => [str_utf8],
380 :op => '=', :values => [str_utf8],
381 :format => 'csv'
381 :format => 'csv'
382 assert_equal 'text/csv', @response.content_type
382 assert_equal 'text/csv', @response.content_type
383 lines = @response.body.chomp.split("\n")
383 lines = @response.body.chomp.split("\n")
384 s1 = "\xaa\xac\xbaA"
384 s1 = "\xaa\xac\xbaA"
385 if str_utf8.respond_to?(:force_encoding)
385 if str_utf8.respond_to?(:force_encoding)
386 s1.force_encoding('Big5')
386 s1.force_encoding('Big5')
387 end
387 end
388 assert lines[0].include?(s1)
388 assert lines[0].include?(s1)
389 assert lines[1].include?(str_big5)
389 assert lines[1].include?(str_big5)
390 end
390 end
391 end
391 end
392
392
393 def test_index_csv_cannot_convert_should_be_replaced_big_5
393 def test_index_csv_cannot_convert_should_be_replaced_big_5
394 with_settings :default_language => "zh-TW" do
394 with_settings :default_language => "zh-TW" do
395 str_utf8 = "\xe4\xbb\xa5\xe5\x86\x85"
395 str_utf8 = "\xe4\xbb\xa5\xe5\x86\x85"
396 if str_utf8.respond_to?(:force_encoding)
396 if str_utf8.respond_to?(:force_encoding)
397 str_utf8.force_encoding('UTF-8')
397 str_utf8.force_encoding('UTF-8')
398 end
398 end
399 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
399 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
400 :status_id => 1, :priority => IssuePriority.all.first,
400 :status_id => 1, :priority => IssuePriority.all.first,
401 :subject => str_utf8)
401 :subject => str_utf8)
402 assert issue.save
402 assert issue.save
403
403
404 get :index, :project_id => 1,
404 get :index, :project_id => 1,
405 :f => ['subject'],
405 :f => ['subject'],
406 :op => '=', :values => [str_utf8],
406 :op => '=', :values => [str_utf8],
407 :c => ['status', 'subject'],
407 :c => ['status', 'subject'],
408 :format => 'csv',
408 :format => 'csv',
409 :set_filter => 1
409 :set_filter => 1
410 assert_equal 'text/csv', @response.content_type
410 assert_equal 'text/csv', @response.content_type
411 lines = @response.body.chomp.split("\n")
411 lines = @response.body.chomp.split("\n")
412 s1 = "\xaa\xac\xbaA" # status
412 s1 = "\xaa\xac\xbaA" # status
413 if str_utf8.respond_to?(:force_encoding)
413 if str_utf8.respond_to?(:force_encoding)
414 s1.force_encoding('Big5')
414 s1.force_encoding('Big5')
415 end
415 end
416 assert lines[0].include?(s1)
416 assert lines[0].include?(s1)
417 s2 = lines[1].split(",")[2]
417 s2 = lines[1].split(",")[2]
418 if s1.respond_to?(:force_encoding)
418 if s1.respond_to?(:force_encoding)
419 s3 = "\xa5H?" # subject
419 s3 = "\xa5H?" # subject
420 s3.force_encoding('Big5')
420 s3.force_encoding('Big5')
421 assert_equal s3, s2
421 assert_equal s3, s2
422 elsif RUBY_PLATFORM == 'java'
422 elsif RUBY_PLATFORM == 'java'
423 assert_equal "??", s2
423 assert_equal "??", s2
424 else
424 else
425 assert_equal "\xa5H???", s2
425 assert_equal "\xa5H???", s2
426 end
426 end
427 end
427 end
428 end
428 end
429
429
430 def test_index_csv_tw
430 def test_index_csv_tw
431 with_settings :default_language => "zh-TW" do
431 with_settings :default_language => "zh-TW" do
432 str1 = "test_index_csv_tw"
432 str1 = "test_index_csv_tw"
433 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
433 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
434 :status_id => 1, :priority => IssuePriority.all.first,
434 :status_id => 1, :priority => IssuePriority.all.first,
435 :subject => str1, :estimated_hours => '1234.5')
435 :subject => str1, :estimated_hours => '1234.5')
436 assert issue.save
436 assert issue.save
437 assert_equal 1234.5, issue.estimated_hours
437 assert_equal 1234.5, issue.estimated_hours
438
438
439 get :index, :project_id => 1,
439 get :index, :project_id => 1,
440 :f => ['subject'],
440 :f => ['subject'],
441 :op => '=', :values => [str1],
441 :op => '=', :values => [str1],
442 :c => ['estimated_hours', 'subject'],
442 :c => ['estimated_hours', 'subject'],
443 :format => 'csv',
443 :format => 'csv',
444 :set_filter => 1
444 :set_filter => 1
445 assert_equal 'text/csv', @response.content_type
445 assert_equal 'text/csv', @response.content_type
446 lines = @response.body.chomp.split("\n")
446 lines = @response.body.chomp.split("\n")
447 assert_equal "#{issue.id},1234.50,#{str1}", lines[1]
447 assert_equal "#{issue.id},1234.50,#{str1}", lines[1]
448
448
449 str_tw = "Traditional Chinese (\xe7\xb9\x81\xe9\xab\x94\xe4\xb8\xad\xe6\x96\x87)"
449 str_tw = "Traditional Chinese (\xe7\xb9\x81\xe9\xab\x94\xe4\xb8\xad\xe6\x96\x87)"
450 if str_tw.respond_to?(:force_encoding)
450 if str_tw.respond_to?(:force_encoding)
451 str_tw.force_encoding('UTF-8')
451 str_tw.force_encoding('UTF-8')
452 end
452 end
453 assert_equal str_tw, l(:general_lang_name)
453 assert_equal str_tw, l(:general_lang_name)
454 assert_equal ',', l(:general_csv_separator)
454 assert_equal ',', l(:general_csv_separator)
455 assert_equal '.', l(:general_csv_decimal_separator)
455 assert_equal '.', l(:general_csv_decimal_separator)
456 end
456 end
457 end
457 end
458
458
459 def test_index_csv_fr
459 def test_index_csv_fr
460 with_settings :default_language => "fr" do
460 with_settings :default_language => "fr" do
461 str1 = "test_index_csv_fr"
461 str1 = "test_index_csv_fr"
462 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
462 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
463 :status_id => 1, :priority => IssuePriority.all.first,
463 :status_id => 1, :priority => IssuePriority.all.first,
464 :subject => str1, :estimated_hours => '1234.5')
464 :subject => str1, :estimated_hours => '1234.5')
465 assert issue.save
465 assert issue.save
466 assert_equal 1234.5, issue.estimated_hours
466 assert_equal 1234.5, issue.estimated_hours
467
467
468 get :index, :project_id => 1,
468 get :index, :project_id => 1,
469 :f => ['subject'],
469 :f => ['subject'],
470 :op => '=', :values => [str1],
470 :op => '=', :values => [str1],
471 :c => ['estimated_hours', 'subject'],
471 :c => ['estimated_hours', 'subject'],
472 :format => 'csv',
472 :format => 'csv',
473 :set_filter => 1
473 :set_filter => 1
474 assert_equal 'text/csv', @response.content_type
474 assert_equal 'text/csv', @response.content_type
475 lines = @response.body.chomp.split("\n")
475 lines = @response.body.chomp.split("\n")
476 assert_equal "#{issue.id};1234,50;#{str1}", lines[1]
476 assert_equal "#{issue.id};1234,50;#{str1}", lines[1]
477
477
478 str_fr = "Fran\xc3\xa7ais"
478 str_fr = "Fran\xc3\xa7ais"
479 if str_fr.respond_to?(:force_encoding)
479 if str_fr.respond_to?(:force_encoding)
480 str_fr.force_encoding('UTF-8')
480 str_fr.force_encoding('UTF-8')
481 end
481 end
482 assert_equal str_fr, l(:general_lang_name)
482 assert_equal str_fr, l(:general_lang_name)
483 assert_equal ';', l(:general_csv_separator)
483 assert_equal ';', l(:general_csv_separator)
484 assert_equal ',', l(:general_csv_decimal_separator)
484 assert_equal ',', l(:general_csv_decimal_separator)
485 end
485 end
486 end
486 end
487
487
488 def test_index_pdf
488 def test_index_pdf
489 ["en", "zh", "zh-TW", "ja", "ko"].each do |lang|
489 ["en", "zh", "zh-TW", "ja", "ko"].each do |lang|
490 with_settings :default_language => lang do
490 with_settings :default_language => lang do
491
491
492 get :index
492 get :index
493 assert_response :success
493 assert_response :success
494 assert_template 'index'
494 assert_template 'index'
495
495
496 if lang == "ja"
496 if lang == "ja"
497 if RUBY_PLATFORM != 'java'
497 if RUBY_PLATFORM != 'java'
498 assert_equal "CP932", l(:general_pdf_encoding)
498 assert_equal "CP932", l(:general_pdf_encoding)
499 end
499 end
500 if RUBY_PLATFORM == 'java' && l(:general_pdf_encoding) == "CP932"
500 if RUBY_PLATFORM == 'java' && l(:general_pdf_encoding) == "CP932"
501 next
501 next
502 end
502 end
503 end
503 end
504
504
505 get :index, :format => 'pdf'
505 get :index, :format => 'pdf'
506 assert_response :success
506 assert_response :success
507 assert_not_nil assigns(:issues)
507 assert_not_nil assigns(:issues)
508 assert_equal 'application/pdf', @response.content_type
508 assert_equal 'application/pdf', @response.content_type
509
509
510 get :index, :project_id => 1, :format => 'pdf'
510 get :index, :project_id => 1, :format => 'pdf'
511 assert_response :success
511 assert_response :success
512 assert_not_nil assigns(:issues)
512 assert_not_nil assigns(:issues)
513 assert_equal 'application/pdf', @response.content_type
513 assert_equal 'application/pdf', @response.content_type
514
514
515 get :index, :project_id => 1, :query_id => 6, :format => 'pdf'
515 get :index, :project_id => 1, :query_id => 6, :format => 'pdf'
516 assert_response :success
516 assert_response :success
517 assert_not_nil assigns(:issues)
517 assert_not_nil assigns(:issues)
518 assert_equal 'application/pdf', @response.content_type
518 assert_equal 'application/pdf', @response.content_type
519 end
519 end
520 end
520 end
521 end
521 end
522
522
523 def test_index_pdf_with_query_grouped_by_list_custom_field
523 def test_index_pdf_with_query_grouped_by_list_custom_field
524 get :index, :project_id => 1, :query_id => 9, :format => 'pdf'
524 get :index, :project_id => 1, :query_id => 9, :format => 'pdf'
525 assert_response :success
525 assert_response :success
526 assert_not_nil assigns(:issues)
526 assert_not_nil assigns(:issues)
527 assert_not_nil assigns(:issue_count_by_group)
527 assert_not_nil assigns(:issue_count_by_group)
528 assert_equal 'application/pdf', @response.content_type
528 assert_equal 'application/pdf', @response.content_type
529 end
529 end
530
530
531 def test_index_sort
531 def test_index_sort
532 get :index, :sort => 'tracker,id:desc'
532 get :index, :sort => 'tracker,id:desc'
533 assert_response :success
533 assert_response :success
534
534
535 sort_params = @request.session['issues_index_sort']
535 sort_params = @request.session['issues_index_sort']
536 assert sort_params.is_a?(String)
536 assert sort_params.is_a?(String)
537 assert_equal 'tracker,id:desc', sort_params
537 assert_equal 'tracker,id:desc', sort_params
538
538
539 issues = assigns(:issues)
539 issues = assigns(:issues)
540 assert_not_nil issues
540 assert_not_nil issues
541 assert !issues.empty?
541 assert !issues.empty?
542 assert_equal issues.sort {|a,b| a.tracker == b.tracker ? b.id <=> a.id : a.tracker <=> b.tracker }.collect(&:id), issues.collect(&:id)
542 assert_equal issues.sort {|a,b| a.tracker == b.tracker ? b.id <=> a.id : a.tracker <=> b.tracker }.collect(&:id), issues.collect(&:id)
543 end
543 end
544
544
545 def test_index_sort_by_field_not_included_in_columns
545 def test_index_sort_by_field_not_included_in_columns
546 Setting.issue_list_default_columns = %w(subject author)
546 Setting.issue_list_default_columns = %w(subject author)
547 get :index, :sort => 'tracker'
547 get :index, :sort => 'tracker'
548 end
548 end
549
549
550 def test_index_sort_by_assigned_to
550 def test_index_sort_by_assigned_to
551 get :index, :sort => 'assigned_to'
551 get :index, :sort => 'assigned_to'
552 assert_response :success
552 assert_response :success
553 assignees = assigns(:issues).collect(&:assigned_to).compact
553 assignees = assigns(:issues).collect(&:assigned_to).compact
554 assert_equal assignees.sort, assignees
554 assert_equal assignees.sort, assignees
555 end
555 end
556
556
557 def test_index_sort_by_assigned_to_desc
557 def test_index_sort_by_assigned_to_desc
558 get :index, :sort => 'assigned_to:desc'
558 get :index, :sort => 'assigned_to:desc'
559 assert_response :success
559 assert_response :success
560 assignees = assigns(:issues).collect(&:assigned_to).compact
560 assignees = assigns(:issues).collect(&:assigned_to).compact
561 assert_equal assignees.sort.reverse, assignees
561 assert_equal assignees.sort.reverse, assignees
562 end
562 end
563
563
564 def test_index_group_by_assigned_to
564 def test_index_group_by_assigned_to
565 get :index, :group_by => 'assigned_to', :sort => 'priority'
565 get :index, :group_by => 'assigned_to', :sort => 'priority'
566 assert_response :success
566 assert_response :success
567 end
567 end
568
568
569 def test_index_sort_by_author
569 def test_index_sort_by_author
570 get :index, :sort => 'author'
570 get :index, :sort => 'author'
571 assert_response :success
571 assert_response :success
572 authors = assigns(:issues).collect(&:author)
572 authors = assigns(:issues).collect(&:author)
573 assert_equal authors.sort, authors
573 assert_equal authors.sort, authors
574 end
574 end
575
575
576 def test_index_sort_by_author_desc
576 def test_index_sort_by_author_desc
577 get :index, :sort => 'author:desc'
577 get :index, :sort => 'author:desc'
578 assert_response :success
578 assert_response :success
579 authors = assigns(:issues).collect(&:author)
579 authors = assigns(:issues).collect(&:author)
580 assert_equal authors.sort.reverse, authors
580 assert_equal authors.sort.reverse, authors
581 end
581 end
582
582
583 def test_index_group_by_author
583 def test_index_group_by_author
584 get :index, :group_by => 'author', :sort => 'priority'
584 get :index, :group_by => 'author', :sort => 'priority'
585 assert_response :success
585 assert_response :success
586 end
586 end
587
587
588 def test_index_sort_by_spent_hours
588 def test_index_sort_by_spent_hours
589 get :index, :sort => 'spent_hours:desc'
589 get :index, :sort => 'spent_hours:desc'
590 assert_response :success
590 assert_response :success
591 hours = assigns(:issues).collect(&:spent_hours)
591 hours = assigns(:issues).collect(&:spent_hours)
592 assert_equal hours.sort.reverse, hours
592 assert_equal hours.sort.reverse, hours
593 end
593 end
594
594
595 def test_index_with_columns
595 def test_index_with_columns
596 columns = ['tracker', 'subject', 'assigned_to']
596 columns = ['tracker', 'subject', 'assigned_to']
597 get :index, :set_filter => 1, :c => columns
597 get :index, :set_filter => 1, :c => columns
598 assert_response :success
598 assert_response :success
599
599
600 # query should use specified columns
600 # query should use specified columns
601 query = assigns(:query)
601 query = assigns(:query)
602 assert_kind_of Query, query
602 assert_kind_of Query, query
603 assert_equal columns, query.column_names.map(&:to_s)
603 assert_equal columns, query.column_names.map(&:to_s)
604
604
605 # columns should be stored in session
605 # columns should be stored in session
606 assert_kind_of Hash, session[:query]
606 assert_kind_of Hash, session[:query]
607 assert_kind_of Array, session[:query][:column_names]
607 assert_kind_of Array, session[:query][:column_names]
608 assert_equal columns, session[:query][:column_names].map(&:to_s)
608 assert_equal columns, session[:query][:column_names].map(&:to_s)
609
609
610 # ensure only these columns are kept in the selected columns list
610 # ensure only these columns are kept in the selected columns list
611 assert_tag :tag => 'select', :attributes => { :id => 'selected_columns' },
611 assert_tag :tag => 'select', :attributes => { :id => 'selected_columns' },
612 :children => { :count => 3 }
612 :children => { :count => 3 }
613 assert_no_tag :tag => 'option', :attributes => { :value => 'project' },
613 assert_no_tag :tag => 'option', :attributes => { :value => 'project' },
614 :parent => { :tag => 'select', :attributes => { :id => "selected_columns" } }
614 :parent => { :tag => 'select', :attributes => { :id => "selected_columns" } }
615 end
615 end
616
616
617 def test_index_without_project_should_implicitly_add_project_column_to_default_columns
617 def test_index_without_project_should_implicitly_add_project_column_to_default_columns
618 Setting.issue_list_default_columns = ['tracker', 'subject', 'assigned_to']
618 Setting.issue_list_default_columns = ['tracker', 'subject', 'assigned_to']
619 get :index, :set_filter => 1
619 get :index, :set_filter => 1
620
620
621 # query should use specified columns
621 # query should use specified columns
622 query = assigns(:query)
622 query = assigns(:query)
623 assert_kind_of Query, query
623 assert_kind_of Query, query
624 assert_equal [:project, :tracker, :subject, :assigned_to], query.columns.map(&:name)
624 assert_equal [:project, :tracker, :subject, :assigned_to], query.columns.map(&:name)
625 end
625 end
626
626
627 def test_index_without_project_and_explicit_default_columns_should_not_add_project_column
627 def test_index_without_project_and_explicit_default_columns_should_not_add_project_column
628 Setting.issue_list_default_columns = ['tracker', 'subject', 'assigned_to']
628 Setting.issue_list_default_columns = ['tracker', 'subject', 'assigned_to']
629 columns = ['tracker', 'subject', 'assigned_to']
629 columns = ['tracker', 'subject', 'assigned_to']
630 get :index, :set_filter => 1, :c => columns
630 get :index, :set_filter => 1, :c => columns
631
631
632 # query should use specified columns
632 # query should use specified columns
633 query = assigns(:query)
633 query = assigns(:query)
634 assert_kind_of Query, query
634 assert_kind_of Query, query
635 assert_equal columns.map(&:to_sym), query.columns.map(&:name)
635 assert_equal columns.map(&:to_sym), query.columns.map(&:name)
636 end
636 end
637
637
638 def test_index_with_custom_field_column
638 def test_index_with_custom_field_column
639 columns = %w(tracker subject cf_2)
639 columns = %w(tracker subject cf_2)
640 get :index, :set_filter => 1, :c => columns
640 get :index, :set_filter => 1, :c => columns
641 assert_response :success
641 assert_response :success
642
642
643 # query should use specified columns
643 # query should use specified columns
644 query = assigns(:query)
644 query = assigns(:query)
645 assert_kind_of Query, query
645 assert_kind_of Query, query
646 assert_equal columns, query.column_names.map(&:to_s)
646 assert_equal columns, query.column_names.map(&:to_s)
647
647
648 assert_tag :td,
648 assert_tag :td,
649 :attributes => {:class => 'cf_2 string'},
649 :attributes => {:class => 'cf_2 string'},
650 :ancestor => {:tag => 'table', :attributes => {:class => /issues/}}
650 :ancestor => {:tag => 'table', :attributes => {:class => /issues/}}
651 end
651 end
652
652
653 def test_index_with_multi_custom_field_column
653 def test_index_with_multi_custom_field_column
654 field = CustomField.find(1)
654 field = CustomField.find(1)
655 field.update_attribute :multiple, true
655 field.update_attribute :multiple, true
656 issue = Issue.find(1)
656 issue = Issue.find(1)
657 issue.custom_field_values = {1 => ['MySQL', 'Oracle']}
657 issue.custom_field_values = {1 => ['MySQL', 'Oracle']}
658 issue.save!
658 issue.save!
659
659
660 get :index, :set_filter => 1, :c => %w(tracker subject cf_1)
660 get :index, :set_filter => 1, :c => %w(tracker subject cf_1)
661 assert_response :success
661 assert_response :success
662
662
663 assert_tag :td,
663 assert_tag :td,
664 :attributes => {:class => /cf_1/},
664 :attributes => {:class => /cf_1/},
665 :content => 'MySQL, Oracle'
665 :content => 'MySQL, Oracle'
666 end
666 end
667
667
668 def test_index_with_multi_user_custom_field_column
668 def test_index_with_multi_user_custom_field_column
669 field = IssueCustomField.create!(:name => 'Multi user', :field_format => 'user', :multiple => true,
669 field = IssueCustomField.create!(:name => 'Multi user', :field_format => 'user', :multiple => true,
670 :tracker_ids => [1], :is_for_all => true)
670 :tracker_ids => [1], :is_for_all => true)
671 issue = Issue.find(1)
671 issue = Issue.find(1)
672 issue.custom_field_values = {field.id => ['2', '3']}
672 issue.custom_field_values = {field.id => ['2', '3']}
673 issue.save!
673 issue.save!
674
674
675 get :index, :set_filter => 1, :c => ['tracker', 'subject', "cf_#{field.id}"]
675 get :index, :set_filter => 1, :c => ['tracker', 'subject', "cf_#{field.id}"]
676 assert_response :success
676 assert_response :success
677
677
678 assert_tag :td,
678 assert_tag :td,
679 :attributes => {:class => /cf_#{field.id}/},
679 :attributes => {:class => /cf_#{field.id}/},
680 :child => {:tag => 'a', :content => 'John Smith'}
680 :child => {:tag => 'a', :content => 'John Smith'}
681 end
681 end
682
682
683 def test_index_with_date_column
683 def test_index_with_date_column
684 Issue.find(1).update_attribute :start_date, '1987-08-24'
684 Issue.find(1).update_attribute :start_date, '1987-08-24'
685
685
686 with_settings :date_format => '%d/%m/%Y' do
686 with_settings :date_format => '%d/%m/%Y' do
687 get :index, :set_filter => 1, :c => %w(start_date)
687 get :index, :set_filter => 1, :c => %w(start_date)
688 assert_tag 'td', :attributes => {:class => /start_date/}, :content => '24/08/1987'
688 assert_tag 'td', :attributes => {:class => /start_date/}, :content => '24/08/1987'
689 end
689 end
690 end
690 end
691
691
692 def test_index_with_done_ratio
692 def test_index_with_done_ratio
693 Issue.find(1).update_attribute :done_ratio, 40
693 Issue.find(1).update_attribute :done_ratio, 40
694
694
695 get :index, :set_filter => 1, :c => %w(done_ratio)
695 get :index, :set_filter => 1, :c => %w(done_ratio)
696 assert_tag 'td', :attributes => {:class => /done_ratio/},
696 assert_tag 'td', :attributes => {:class => /done_ratio/},
697 :child => {:tag => 'table', :attributes => {:class => 'progress'},
697 :child => {:tag => 'table', :attributes => {:class => 'progress'},
698 :descendant => {:tag => 'td', :attributes => {:class => 'closed', :style => 'width: 40%;'}}
698 :descendant => {:tag => 'td', :attributes => {:class => 'closed', :style => 'width: 40%;'}}
699 }
699 }
700 end
700 end
701
701
702 def test_index_with_spent_hours_column
702 def test_index_with_spent_hours_column
703 get :index, :set_filter => 1, :c => %w(subject spent_hours)
703 get :index, :set_filter => 1, :c => %w(subject spent_hours)
704
704
705 assert_tag 'tr', :attributes => {:id => 'issue-3'},
705 assert_tag 'tr', :attributes => {:id => 'issue-3'},
706 :child => {
706 :child => {
707 :tag => 'td', :attributes => {:class => /spent_hours/}, :content => '1.00'
707 :tag => 'td', :attributes => {:class => /spent_hours/}, :content => '1.00'
708 }
708 }
709 end
709 end
710
710
711 def test_index_should_not_show_spent_hours_column_without_permission
711 def test_index_should_not_show_spent_hours_column_without_permission
712 Role.anonymous.remove_permission! :view_time_entries
712 Role.anonymous.remove_permission! :view_time_entries
713 get :index, :set_filter => 1, :c => %w(subject spent_hours)
713 get :index, :set_filter => 1, :c => %w(subject spent_hours)
714
714
715 assert_no_tag 'td', :attributes => {:class => /spent_hours/}
715 assert_no_tag 'td', :attributes => {:class => /spent_hours/}
716 end
716 end
717
717
718 def test_index_with_fixed_version
718 def test_index_with_fixed_version
719 get :index, :set_filter => 1, :c => %w(fixed_version)
719 get :index, :set_filter => 1, :c => %w(fixed_version)
720 assert_tag 'td', :attributes => {:class => /fixed_version/},
720 assert_tag 'td', :attributes => {:class => /fixed_version/},
721 :child => {:tag => 'a', :content => '1.0', :attributes => {:href => '/versions/2'}}
721 :child => {:tag => 'a', :content => '1.0', :attributes => {:href => '/versions/2'}}
722 end
722 end
723
723
724 def test_index_send_html_if_query_is_invalid
724 def test_index_send_html_if_query_is_invalid
725 get :index, :f => ['start_date'], :op => {:start_date => '='}
725 get :index, :f => ['start_date'], :op => {:start_date => '='}
726 assert_equal 'text/html', @response.content_type
726 assert_equal 'text/html', @response.content_type
727 assert_template 'index'
727 assert_template 'index'
728 end
728 end
729
729
730 def test_index_send_nothing_if_query_is_invalid
730 def test_index_send_nothing_if_query_is_invalid
731 get :index, :f => ['start_date'], :op => {:start_date => '='}, :format => 'csv'
731 get :index, :f => ['start_date'], :op => {:start_date => '='}, :format => 'csv'
732 assert_equal 'text/csv', @response.content_type
732 assert_equal 'text/csv', @response.content_type
733 assert @response.body.blank?
733 assert @response.body.blank?
734 end
734 end
735
735
736 def test_show_by_anonymous
736 def test_show_by_anonymous
737 get :show, :id => 1
737 get :show, :id => 1
738 assert_response :success
738 assert_response :success
739 assert_template 'show'
739 assert_template 'show'
740 assert_not_nil assigns(:issue)
740 assert_not_nil assigns(:issue)
741 assert_equal Issue.find(1), assigns(:issue)
741 assert_equal Issue.find(1), assigns(:issue)
742
742
743 # anonymous role is allowed to add a note
743 # anonymous role is allowed to add a note
744 assert_tag :tag => 'form',
744 assert_tag :tag => 'form',
745 :descendant => { :tag => 'fieldset',
745 :descendant => { :tag => 'fieldset',
746 :child => { :tag => 'legend',
746 :child => { :tag => 'legend',
747 :content => /Notes/ } }
747 :content => /Notes/ } }
748 assert_tag :tag => 'title',
748 assert_tag :tag => 'title',
749 :content => "Bug #1: Can't print recipes - eCookbook - Redmine"
749 :content => "Bug #1: Can't print recipes - eCookbook - Redmine"
750 end
750 end
751
751
752 def test_show_by_manager
752 def test_show_by_manager
753 @request.session[:user_id] = 2
753 @request.session[:user_id] = 2
754 get :show, :id => 1
754 get :show, :id => 1
755 assert_response :success
755 assert_response :success
756
756
757 assert_tag :tag => 'a',
757 assert_tag :tag => 'a',
758 :content => /Quote/
758 :content => /Quote/
759
759
760 assert_tag :tag => 'form',
760 assert_tag :tag => 'form',
761 :descendant => { :tag => 'fieldset',
761 :descendant => { :tag => 'fieldset',
762 :child => { :tag => 'legend',
762 :child => { :tag => 'legend',
763 :content => /Change properties/ } },
763 :content => /Change properties/ } },
764 :descendant => { :tag => 'fieldset',
764 :descendant => { :tag => 'fieldset',
765 :child => { :tag => 'legend',
765 :child => { :tag => 'legend',
766 :content => /Log time/ } },
766 :content => /Log time/ } },
767 :descendant => { :tag => 'fieldset',
767 :descendant => { :tag => 'fieldset',
768 :child => { :tag => 'legend',
768 :child => { :tag => 'legend',
769 :content => /Notes/ } }
769 :content => /Notes/ } }
770 end
770 end
771
771
772 def test_show_should_display_update_form
772 def test_show_should_display_update_form
773 @request.session[:user_id] = 2
773 @request.session[:user_id] = 2
774 get :show, :id => 1
774 get :show, :id => 1
775 assert_response :success
775 assert_response :success
776
776
777 assert_tag 'form', :attributes => {:id => 'issue-form'}
777 assert_tag 'form', :attributes => {:id => 'issue-form'}
778 assert_tag 'input', :attributes => {:name => 'issue[is_private]'}
778 assert_tag 'input', :attributes => {:name => 'issue[is_private]'}
779 assert_tag 'select', :attributes => {:name => 'issue[project_id]'}
779 assert_tag 'select', :attributes => {:name => 'issue[project_id]'}
780 assert_tag 'select', :attributes => {:name => 'issue[tracker_id]'}
780 assert_tag 'select', :attributes => {:name => 'issue[tracker_id]'}
781 assert_tag 'input', :attributes => {:name => 'issue[subject]'}
781 assert_tag 'input', :attributes => {:name => 'issue[subject]'}
782 assert_tag 'textarea', :attributes => {:name => 'issue[description]'}
782 assert_tag 'textarea', :attributes => {:name => 'issue[description]'}
783 assert_tag 'select', :attributes => {:name => 'issue[status_id]'}
783 assert_tag 'select', :attributes => {:name => 'issue[status_id]'}
784 assert_tag 'select', :attributes => {:name => 'issue[priority_id]'}
784 assert_tag 'select', :attributes => {:name => 'issue[priority_id]'}
785 assert_tag 'select', :attributes => {:name => 'issue[assigned_to_id]'}
785 assert_tag 'select', :attributes => {:name => 'issue[assigned_to_id]'}
786 assert_tag 'select', :attributes => {:name => 'issue[category_id]'}
786 assert_tag 'select', :attributes => {:name => 'issue[category_id]'}
787 assert_tag 'select', :attributes => {:name => 'issue[fixed_version_id]'}
787 assert_tag 'select', :attributes => {:name => 'issue[fixed_version_id]'}
788 assert_tag 'input', :attributes => {:name => 'issue[parent_issue_id]'}
788 assert_tag 'input', :attributes => {:name => 'issue[parent_issue_id]'}
789 assert_tag 'input', :attributes => {:name => 'issue[start_date]'}
789 assert_tag 'input', :attributes => {:name => 'issue[start_date]'}
790 assert_tag 'input', :attributes => {:name => 'issue[due_date]'}
790 assert_tag 'input', :attributes => {:name => 'issue[due_date]'}
791 assert_tag 'select', :attributes => {:name => 'issue[done_ratio]'}
791 assert_tag 'select', :attributes => {:name => 'issue[done_ratio]'}
792 assert_tag 'input', :attributes => { :name => 'issue[custom_field_values][2]' }
792 assert_tag 'input', :attributes => { :name => 'issue[custom_field_values][2]' }
793 assert_no_tag 'input', :attributes => {:name => 'issue[watcher_user_ids][]'}
793 assert_no_tag 'input', :attributes => {:name => 'issue[watcher_user_ids][]'}
794 assert_tag 'textarea', :attributes => {:name => 'notes'}
794 assert_tag 'textarea', :attributes => {:name => 'notes'}
795 end
795 end
796
796
797 def test_show_should_display_update_form_with_minimal_permissions
797 def test_show_should_display_update_form_with_minimal_permissions
798 Role.find(1).update_attribute :permissions, [:view_issues, :add_issue_notes]
798 Role.find(1).update_attribute :permissions, [:view_issues, :add_issue_notes]
799 Workflow.delete_all :role_id => 1
799 Workflow.delete_all :role_id => 1
800
800
801 @request.session[:user_id] = 2
801 @request.session[:user_id] = 2
802 get :show, :id => 1
802 get :show, :id => 1
803 assert_response :success
803 assert_response :success
804
804
805 assert_tag 'form', :attributes => {:id => 'issue-form'}
805 assert_tag 'form', :attributes => {:id => 'issue-form'}
806 assert_no_tag 'input', :attributes => {:name => 'issue[is_private]'}
806 assert_no_tag 'input', :attributes => {:name => 'issue[is_private]'}
807 assert_no_tag 'select', :attributes => {:name => 'issue[project_id]'}
807 assert_no_tag 'select', :attributes => {:name => 'issue[project_id]'}
808 assert_no_tag 'select', :attributes => {:name => 'issue[tracker_id]'}
808 assert_no_tag 'select', :attributes => {:name => 'issue[tracker_id]'}
809 assert_no_tag 'input', :attributes => {:name => 'issue[subject]'}
809 assert_no_tag 'input', :attributes => {:name => 'issue[subject]'}
810 assert_no_tag 'textarea', :attributes => {:name => 'issue[description]'}
810 assert_no_tag 'textarea', :attributes => {:name => 'issue[description]'}
811 assert_no_tag 'select', :attributes => {:name => 'issue[status_id]'}
811 assert_no_tag 'select', :attributes => {:name => 'issue[status_id]'}
812 assert_no_tag 'select', :attributes => {:name => 'issue[priority_id]'}
812 assert_no_tag 'select', :attributes => {:name => 'issue[priority_id]'}
813 assert_no_tag 'select', :attributes => {:name => 'issue[assigned_to_id]'}
813 assert_no_tag 'select', :attributes => {:name => 'issue[assigned_to_id]'}
814 assert_no_tag 'select', :attributes => {:name => 'issue[category_id]'}
814 assert_no_tag 'select', :attributes => {:name => 'issue[category_id]'}
815 assert_no_tag 'select', :attributes => {:name => 'issue[fixed_version_id]'}
815 assert_no_tag 'select', :attributes => {:name => 'issue[fixed_version_id]'}
816 assert_no_tag 'input', :attributes => {:name => 'issue[parent_issue_id]'}
816 assert_no_tag 'input', :attributes => {:name => 'issue[parent_issue_id]'}
817 assert_no_tag 'input', :attributes => {:name => 'issue[start_date]'}
817 assert_no_tag 'input', :attributes => {:name => 'issue[start_date]'}
818 assert_no_tag 'input', :attributes => {:name => 'issue[due_date]'}
818 assert_no_tag 'input', :attributes => {:name => 'issue[due_date]'}
819 assert_no_tag 'select', :attributes => {:name => 'issue[done_ratio]'}
819 assert_no_tag 'select', :attributes => {:name => 'issue[done_ratio]'}
820 assert_no_tag 'input', :attributes => { :name => 'issue[custom_field_values][2]' }
820 assert_no_tag 'input', :attributes => { :name => 'issue[custom_field_values][2]' }
821 assert_no_tag 'input', :attributes => {:name => 'issue[watcher_user_ids][]'}
821 assert_no_tag 'input', :attributes => {:name => 'issue[watcher_user_ids][]'}
822 assert_tag 'textarea', :attributes => {:name => 'notes'}
822 assert_tag 'textarea', :attributes => {:name => 'notes'}
823 end
823 end
824
824
825 def test_show_should_display_update_form_with_workflow_permissions
825 def test_show_should_display_update_form_with_workflow_permissions
826 Role.find(1).update_attribute :permissions, [:view_issues, :add_issue_notes]
826 Role.find(1).update_attribute :permissions, [:view_issues, :add_issue_notes]
827
827
828 @request.session[:user_id] = 2
828 @request.session[:user_id] = 2
829 get :show, :id => 1
829 get :show, :id => 1
830 assert_response :success
830 assert_response :success
831
831
832 assert_tag 'form', :attributes => {:id => 'issue-form'}
832 assert_tag 'form', :attributes => {:id => 'issue-form'}
833 assert_no_tag 'input', :attributes => {:name => 'issue[is_private]'}
833 assert_no_tag 'input', :attributes => {:name => 'issue[is_private]'}
834 assert_no_tag 'select', :attributes => {:name => 'issue[project_id]'}
834 assert_no_tag 'select', :attributes => {:name => 'issue[project_id]'}
835 assert_no_tag 'select', :attributes => {:name => 'issue[tracker_id]'}
835 assert_no_tag 'select', :attributes => {:name => 'issue[tracker_id]'}
836 assert_no_tag 'input', :attributes => {:name => 'issue[subject]'}
836 assert_no_tag 'input', :attributes => {:name => 'issue[subject]'}
837 assert_no_tag 'textarea', :attributes => {:name => 'issue[description]'}
837 assert_no_tag 'textarea', :attributes => {:name => 'issue[description]'}
838 assert_tag 'select', :attributes => {:name => 'issue[status_id]'}
838 assert_tag 'select', :attributes => {:name => 'issue[status_id]'}
839 assert_no_tag 'select', :attributes => {:name => 'issue[priority_id]'}
839 assert_no_tag 'select', :attributes => {:name => 'issue[priority_id]'}
840 assert_tag 'select', :attributes => {:name => 'issue[assigned_to_id]'}
840 assert_tag 'select', :attributes => {:name => 'issue[assigned_to_id]'}
841 assert_no_tag 'select', :attributes => {:name => 'issue[category_id]'}
841 assert_no_tag 'select', :attributes => {:name => 'issue[category_id]'}
842 assert_tag 'select', :attributes => {:name => 'issue[fixed_version_id]'}
842 assert_tag 'select', :attributes => {:name => 'issue[fixed_version_id]'}
843 assert_no_tag 'input', :attributes => {:name => 'issue[parent_issue_id]'}
843 assert_no_tag 'input', :attributes => {:name => 'issue[parent_issue_id]'}
844 assert_no_tag 'input', :attributes => {:name => 'issue[start_date]'}
844 assert_no_tag 'input', :attributes => {:name => 'issue[start_date]'}
845 assert_no_tag 'input', :attributes => {:name => 'issue[due_date]'}
845 assert_no_tag 'input', :attributes => {:name => 'issue[due_date]'}
846 assert_tag 'select', :attributes => {:name => 'issue[done_ratio]'}
846 assert_tag 'select', :attributes => {:name => 'issue[done_ratio]'}
847 assert_no_tag 'input', :attributes => { :name => 'issue[custom_field_values][2]' }
847 assert_no_tag 'input', :attributes => { :name => 'issue[custom_field_values][2]' }
848 assert_no_tag 'input', :attributes => {:name => 'issue[watcher_user_ids][]'}
848 assert_no_tag 'input', :attributes => {:name => 'issue[watcher_user_ids][]'}
849 assert_tag 'textarea', :attributes => {:name => 'notes'}
849 assert_tag 'textarea', :attributes => {:name => 'notes'}
850 end
850 end
851
851
852 def test_show_should_not_display_update_form_without_permissions
852 def test_show_should_not_display_update_form_without_permissions
853 Role.find(1).update_attribute :permissions, [:view_issues]
853 Role.find(1).update_attribute :permissions, [:view_issues]
854
854
855 @request.session[:user_id] = 2
855 @request.session[:user_id] = 2
856 get :show, :id => 1
856 get :show, :id => 1
857 assert_response :success
857 assert_response :success
858
858
859 assert_no_tag 'form', :attributes => {:id => 'issue-form'}
859 assert_no_tag 'form', :attributes => {:id => 'issue-form'}
860 end
860 end
861
861
862 def test_update_form_should_not_display_inactive_enumerations
862 def test_update_form_should_not_display_inactive_enumerations
863 @request.session[:user_id] = 2
863 @request.session[:user_id] = 2
864 get :show, :id => 1
864 get :show, :id => 1
865 assert_response :success
865 assert_response :success
866
866
867 assert ! IssuePriority.find(15).active?
867 assert ! IssuePriority.find(15).active?
868 assert_no_tag :option, :attributes => {:value => '15'},
868 assert_no_tag :option, :attributes => {:value => '15'},
869 :parent => {:tag => 'select', :attributes => {:id => 'issue_priority_id'} }
869 :parent => {:tag => 'select', :attributes => {:id => 'issue_priority_id'} }
870 end
870 end
871
871
872 def test_update_form_should_allow_attachment_upload
872 def test_update_form_should_allow_attachment_upload
873 @request.session[:user_id] = 2
873 @request.session[:user_id] = 2
874 get :show, :id => 1
874 get :show, :id => 1
875
875
876 assert_tag :tag => 'form',
876 assert_tag :tag => 'form',
877 :attributes => {:id => 'issue-form', :method => 'post', :enctype => 'multipart/form-data'},
877 :attributes => {:id => 'issue-form', :method => 'post', :enctype => 'multipart/form-data'},
878 :descendant => {
878 :descendant => {
879 :tag => 'input',
879 :tag => 'input',
880 :attributes => {:type => 'file', :name => 'attachments[1][file]'}
880 :attributes => {:type => 'file', :name => 'attachments[1][file]'}
881 }
881 }
882 end
882 end
883
883
884 def test_show_should_deny_anonymous_access_without_permission
884 def test_show_should_deny_anonymous_access_without_permission
885 Role.anonymous.remove_permission!(:view_issues)
885 Role.anonymous.remove_permission!(:view_issues)
886 get :show, :id => 1
886 get :show, :id => 1
887 assert_response :redirect
887 assert_response :redirect
888 end
888 end
889
889
890 def test_show_should_deny_anonymous_access_to_private_issue
890 def test_show_should_deny_anonymous_access_to_private_issue
891 Issue.update_all(["is_private = ?", true], "id = 1")
891 Issue.update_all(["is_private = ?", true], "id = 1")
892 get :show, :id => 1
892 get :show, :id => 1
893 assert_response :redirect
893 assert_response :redirect
894 end
894 end
895
895
896 def test_show_should_deny_non_member_access_without_permission
896 def test_show_should_deny_non_member_access_without_permission
897 Role.non_member.remove_permission!(:view_issues)
897 Role.non_member.remove_permission!(:view_issues)
898 @request.session[:user_id] = 9
898 @request.session[:user_id] = 9
899 get :show, :id => 1
899 get :show, :id => 1
900 assert_response 403
900 assert_response 403
901 end
901 end
902
902
903 def test_show_should_deny_non_member_access_to_private_issue
903 def test_show_should_deny_non_member_access_to_private_issue
904 Issue.update_all(["is_private = ?", true], "id = 1")
904 Issue.update_all(["is_private = ?", true], "id = 1")
905 @request.session[:user_id] = 9
905 @request.session[:user_id] = 9
906 get :show, :id => 1
906 get :show, :id => 1
907 assert_response 403
907 assert_response 403
908 end
908 end
909
909
910 def test_show_should_deny_member_access_without_permission
910 def test_show_should_deny_member_access_without_permission
911 Role.find(1).remove_permission!(:view_issues)
911 Role.find(1).remove_permission!(:view_issues)
912 @request.session[:user_id] = 2
912 @request.session[:user_id] = 2
913 get :show, :id => 1
913 get :show, :id => 1
914 assert_response 403
914 assert_response 403
915 end
915 end
916
916
917 def test_show_should_deny_member_access_to_private_issue_without_permission
917 def test_show_should_deny_member_access_to_private_issue_without_permission
918 Issue.update_all(["is_private = ?", true], "id = 1")
918 Issue.update_all(["is_private = ?", true], "id = 1")
919 @request.session[:user_id] = 3
919 @request.session[:user_id] = 3
920 get :show, :id => 1
920 get :show, :id => 1
921 assert_response 403
921 assert_response 403
922 end
922 end
923
923
924 def test_show_should_allow_author_access_to_private_issue
924 def test_show_should_allow_author_access_to_private_issue
925 Issue.update_all(["is_private = ?, author_id = 3", true], "id = 1")
925 Issue.update_all(["is_private = ?, author_id = 3", true], "id = 1")
926 @request.session[:user_id] = 3
926 @request.session[:user_id] = 3
927 get :show, :id => 1
927 get :show, :id => 1
928 assert_response :success
928 assert_response :success
929 end
929 end
930
930
931 def test_show_should_allow_assignee_access_to_private_issue
931 def test_show_should_allow_assignee_access_to_private_issue
932 Issue.update_all(["is_private = ?, assigned_to_id = 3", true], "id = 1")
932 Issue.update_all(["is_private = ?, assigned_to_id = 3", true], "id = 1")
933 @request.session[:user_id] = 3
933 @request.session[:user_id] = 3
934 get :show, :id => 1
934 get :show, :id => 1
935 assert_response :success
935 assert_response :success
936 end
936 end
937
937
938 def test_show_should_allow_member_access_to_private_issue_with_permission
938 def test_show_should_allow_member_access_to_private_issue_with_permission
939 Issue.update_all(["is_private = ?", true], "id = 1")
939 Issue.update_all(["is_private = ?", true], "id = 1")
940 User.find(3).roles_for_project(Project.find(1)).first.update_attribute :issues_visibility, 'all'
940 User.find(3).roles_for_project(Project.find(1)).first.update_attribute :issues_visibility, 'all'
941 @request.session[:user_id] = 3
941 @request.session[:user_id] = 3
942 get :show, :id => 1
942 get :show, :id => 1
943 assert_response :success
943 assert_response :success
944 end
944 end
945
945
946 def test_show_should_not_disclose_relations_to_invisible_issues
946 def test_show_should_not_disclose_relations_to_invisible_issues
947 Setting.cross_project_issue_relations = '1'
947 Setting.cross_project_issue_relations = '1'
948 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(2), :relation_type => 'relates')
948 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(2), :relation_type => 'relates')
949 # Relation to a private project issue
949 # Relation to a private project issue
950 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(4), :relation_type => 'relates')
950 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(4), :relation_type => 'relates')
951
951
952 get :show, :id => 1
952 get :show, :id => 1
953 assert_response :success
953 assert_response :success
954
954
955 assert_tag :div, :attributes => { :id => 'relations' },
955 assert_tag :div, :attributes => { :id => 'relations' },
956 :descendant => { :tag => 'a', :content => /#2$/ }
956 :descendant => { :tag => 'a', :content => /#2$/ }
957 assert_no_tag :div, :attributes => { :id => 'relations' },
957 assert_no_tag :div, :attributes => { :id => 'relations' },
958 :descendant => { :tag => 'a', :content => /#4$/ }
958 :descendant => { :tag => 'a', :content => /#4$/ }
959 end
959 end
960
960
961 def test_show_should_list_subtasks
961 def test_show_should_list_subtasks
962 Issue.generate!(:project_id => 1, :author_id => 1, :tracker_id => 1, :parent_issue_id => 1, :subject => 'Child Issue')
962 Issue.generate!(:project_id => 1, :author_id => 1, :tracker_id => 1, :parent_issue_id => 1, :subject => 'Child Issue')
963
963
964 get :show, :id => 1
964 get :show, :id => 1
965 assert_response :success
965 assert_response :success
966 assert_tag 'div', :attributes => {:id => 'issue_tree'},
966 assert_tag 'div', :attributes => {:id => 'issue_tree'},
967 :descendant => {:tag => 'td', :content => /Child Issue/, :attributes => {:class => /subject/}}
967 :descendant => {:tag => 'td', :content => /Child Issue/, :attributes => {:class => /subject/}}
968 end
968 end
969
969
970 def test_show_should_list_parents
970 def test_show_should_list_parents
971 issue = Issue.generate!(:project_id => 1, :author_id => 1, :tracker_id => 1, :parent_issue_id => 1, :subject => 'Child Issue')
971 issue = Issue.generate!(:project_id => 1, :author_id => 1, :tracker_id => 1, :parent_issue_id => 1, :subject => 'Child Issue')
972
972
973 get :show, :id => issue.id
973 get :show, :id => issue.id
974 assert_response :success
974 assert_response :success
975 assert_tag 'div', :attributes => {:class => 'subject'},
975 assert_tag 'div', :attributes => {:class => 'subject'},
976 :descendant => {:tag => 'h3', :content => 'Child Issue'}
976 :descendant => {:tag => 'h3', :content => 'Child Issue'}
977 assert_tag 'div', :attributes => {:class => 'subject'},
977 assert_tag 'div', :attributes => {:class => 'subject'},
978 :descendant => {:tag => 'a', :attributes => {:href => '/issues/1'}}
978 :descendant => {:tag => 'a', :attributes => {:href => '/issues/1'}}
979 end
979 end
980
980
981 def test_show_should_not_display_prev_next_links_without_query_in_session
981 def test_show_should_not_display_prev_next_links_without_query_in_session
982 get :show, :id => 1
982 get :show, :id => 1
983 assert_response :success
983 assert_response :success
984 assert_nil assigns(:prev_issue_id)
984 assert_nil assigns(:prev_issue_id)
985 assert_nil assigns(:next_issue_id)
985 assert_nil assigns(:next_issue_id)
986
986
987 assert_no_tag 'div', :attributes => {:class => /next-prev-links/}
987 assert_no_tag 'div', :attributes => {:class => /next-prev-links/}
988 end
988 end
989
989
990 def test_show_should_display_prev_next_links_with_query_in_session
990 def test_show_should_display_prev_next_links_with_query_in_session
991 @request.session[:query] = {:filters => {'status_id' => {:values => [''], :operator => 'o'}}, :project_id => nil}
991 @request.session[:query] = {:filters => {'status_id' => {:values => [''], :operator => 'o'}}, :project_id => nil}
992 @request.session['issues_index_sort'] = 'id'
992 @request.session['issues_index_sort'] = 'id'
993
993
994 with_settings :display_subprojects_issues => '0' do
994 with_settings :display_subprojects_issues => '0' do
995 get :show, :id => 3
995 get :show, :id => 3
996 end
996 end
997
997
998 assert_response :success
998 assert_response :success
999 # Previous and next issues for all projects
999 # Previous and next issues for all projects
1000 assert_equal 2, assigns(:prev_issue_id)
1000 assert_equal 2, assigns(:prev_issue_id)
1001 assert_equal 5, assigns(:next_issue_id)
1001 assert_equal 5, assigns(:next_issue_id)
1002
1002
1003 assert_tag 'div', :attributes => {:class => /next-prev-links/}
1003 assert_tag 'div', :attributes => {:class => /next-prev-links/}
1004 assert_tag 'a', :attributes => {:href => '/issues/2'}, :content => /Previous/
1004 assert_tag 'a', :attributes => {:href => '/issues/2'}, :content => /Previous/
1005 assert_tag 'a', :attributes => {:href => '/issues/5'}, :content => /Next/
1005 assert_tag 'a', :attributes => {:href => '/issues/5'}, :content => /Next/
1006
1006
1007 count = Issue.open.visible.count
1007 count = Issue.open.visible.count
1008 assert_tag 'span', :attributes => {:class => 'position'}, :content => "3 of #{count}"
1008 assert_tag 'span', :attributes => {:class => 'position'}, :content => "3 of #{count}"
1009 end
1009 end
1010
1010
1011 def test_show_should_display_prev_next_links_with_saved_query_in_session
1011 def test_show_should_display_prev_next_links_with_saved_query_in_session
1012 query = Query.create!(:name => 'test', :is_public => true, :user_id => 1,
1012 query = Query.create!(:name => 'test', :is_public => true, :user_id => 1,
1013 :filters => {'status_id' => {:values => ['5'], :operator => '='}},
1013 :filters => {'status_id' => {:values => ['5'], :operator => '='}},
1014 :sort_criteria => [['id', 'asc']])
1014 :sort_criteria => [['id', 'asc']])
1015 @request.session[:query] = {:id => query.id, :project_id => nil}
1015 @request.session[:query] = {:id => query.id, :project_id => nil}
1016
1016
1017 get :show, :id => 11
1017 get :show, :id => 11
1018
1018
1019 assert_response :success
1019 assert_response :success
1020 assert_equal query, assigns(:query)
1020 assert_equal query, assigns(:query)
1021 # Previous and next issues for all projects
1021 # Previous and next issues for all projects
1022 assert_equal 8, assigns(:prev_issue_id)
1022 assert_equal 8, assigns(:prev_issue_id)
1023 assert_equal 12, assigns(:next_issue_id)
1023 assert_equal 12, assigns(:next_issue_id)
1024
1024
1025 assert_tag 'a', :attributes => {:href => '/issues/8'}, :content => /Previous/
1025 assert_tag 'a', :attributes => {:href => '/issues/8'}, :content => /Previous/
1026 assert_tag 'a', :attributes => {:href => '/issues/12'}, :content => /Next/
1026 assert_tag 'a', :attributes => {:href => '/issues/12'}, :content => /Next/
1027 end
1027 end
1028
1028
1029 def test_show_should_display_prev_next_links_with_query_and_sort_on_association
1029 def test_show_should_display_prev_next_links_with_query_and_sort_on_association
1030 @request.session[:query] = {:filters => {'status_id' => {:values => [''], :operator => 'o'}}, :project_id => nil}
1030 @request.session[:query] = {:filters => {'status_id' => {:values => [''], :operator => 'o'}}, :project_id => nil}
1031
1031
1032 %w(project tracker status priority author assigned_to category fixed_version).each do |assoc_sort|
1032 %w(project tracker status priority author assigned_to category fixed_version).each do |assoc_sort|
1033 @request.session['issues_index_sort'] = assoc_sort
1033 @request.session['issues_index_sort'] = assoc_sort
1034
1034
1035 get :show, :id => 3
1035 get :show, :id => 3
1036 assert_response :success, "Wrong response status for #{assoc_sort} sort"
1036 assert_response :success, "Wrong response status for #{assoc_sort} sort"
1037
1037
1038 assert_tag 'a', :content => /Previous/
1038 assert_tag 'a', :content => /Previous/
1039 assert_tag 'a', :content => /Next/
1039 assert_tag 'a', :content => /Next/
1040 end
1040 end
1041 end
1041 end
1042
1042
1043 def test_show_should_display_prev_next_links_with_project_query_in_session
1043 def test_show_should_display_prev_next_links_with_project_query_in_session
1044 @request.session[:query] = {:filters => {'status_id' => {:values => [''], :operator => 'o'}}, :project_id => 1}
1044 @request.session[:query] = {:filters => {'status_id' => {:values => [''], :operator => 'o'}}, :project_id => 1}
1045 @request.session['issues_index_sort'] = 'id'
1045 @request.session['issues_index_sort'] = 'id'
1046
1046
1047 with_settings :display_subprojects_issues => '0' do
1047 with_settings :display_subprojects_issues => '0' do
1048 get :show, :id => 3
1048 get :show, :id => 3
1049 end
1049 end
1050
1050
1051 assert_response :success
1051 assert_response :success
1052 # Previous and next issues inside project
1052 # Previous and next issues inside project
1053 assert_equal 2, assigns(:prev_issue_id)
1053 assert_equal 2, assigns(:prev_issue_id)
1054 assert_equal 7, assigns(:next_issue_id)
1054 assert_equal 7, assigns(:next_issue_id)
1055
1055
1056 assert_tag 'a', :attributes => {:href => '/issues/2'}, :content => /Previous/
1056 assert_tag 'a', :attributes => {:href => '/issues/2'}, :content => /Previous/
1057 assert_tag 'a', :attributes => {:href => '/issues/7'}, :content => /Next/
1057 assert_tag 'a', :attributes => {:href => '/issues/7'}, :content => /Next/
1058 end
1058 end
1059
1059
1060 def test_show_should_not_display_prev_link_for_first_issue
1060 def test_show_should_not_display_prev_link_for_first_issue
1061 @request.session[:query] = {:filters => {'status_id' => {:values => [''], :operator => 'o'}}, :project_id => 1}
1061 @request.session[:query] = {:filters => {'status_id' => {:values => [''], :operator => 'o'}}, :project_id => 1}
1062 @request.session['issues_index_sort'] = 'id'
1062 @request.session['issues_index_sort'] = 'id'
1063
1063
1064 with_settings :display_subprojects_issues => '0' do
1064 with_settings :display_subprojects_issues => '0' do
1065 get :show, :id => 1
1065 get :show, :id => 1
1066 end
1066 end
1067
1067
1068 assert_response :success
1068 assert_response :success
1069 assert_nil assigns(:prev_issue_id)
1069 assert_nil assigns(:prev_issue_id)
1070 assert_equal 2, assigns(:next_issue_id)
1070 assert_equal 2, assigns(:next_issue_id)
1071
1071
1072 assert_no_tag 'a', :content => /Previous/
1072 assert_no_tag 'a', :content => /Previous/
1073 assert_tag 'a', :attributes => {:href => '/issues/2'}, :content => /Next/
1073 assert_tag 'a', :attributes => {:href => '/issues/2'}, :content => /Next/
1074 end
1074 end
1075
1075
1076 def test_show_should_not_display_prev_next_links_for_issue_not_in_query_results
1076 def test_show_should_not_display_prev_next_links_for_issue_not_in_query_results
1077 @request.session[:query] = {:filters => {'status_id' => {:values => [''], :operator => 'c'}}, :project_id => 1}
1077 @request.session[:query] = {:filters => {'status_id' => {:values => [''], :operator => 'c'}}, :project_id => 1}
1078 @request.session['issues_index_sort'] = 'id'
1078 @request.session['issues_index_sort'] = 'id'
1079
1079
1080 get :show, :id => 1
1080 get :show, :id => 1
1081
1081
1082 assert_response :success
1082 assert_response :success
1083 assert_nil assigns(:prev_issue_id)
1083 assert_nil assigns(:prev_issue_id)
1084 assert_nil assigns(:next_issue_id)
1084 assert_nil assigns(:next_issue_id)
1085
1085
1086 assert_no_tag 'a', :content => /Previous/
1086 assert_no_tag 'a', :content => /Previous/
1087 assert_no_tag 'a', :content => /Next/
1087 assert_no_tag 'a', :content => /Next/
1088 end
1088 end
1089
1089
1090 def test_show_should_display_visible_changesets_from_other_projects
1090 def test_show_should_display_visible_changesets_from_other_projects
1091 project = Project.find(2)
1091 project = Project.find(2)
1092 issue = project.issues.first
1092 issue = project.issues.first
1093 issue.changeset_ids = [102]
1093 issue.changeset_ids = [102]
1094 issue.save!
1094 issue.save!
1095 project.disable_module! :repository
1095 project.disable_module! :repository
1096
1096
1097 @request.session[:user_id] = 2
1097 @request.session[:user_id] = 2
1098 get :show, :id => issue.id
1098 get :show, :id => issue.id
1099 assert_tag 'a', :attributes => {:href => "/projects/ecookbook/repository/revisions/3"}
1099 assert_tag 'a', :attributes => {:href => "/projects/ecookbook/repository/revisions/3"}
1100 end
1100 end
1101
1101
1102 def test_show_with_multi_custom_field
1102 def test_show_with_multi_custom_field
1103 field = CustomField.find(1)
1103 field = CustomField.find(1)
1104 field.update_attribute :multiple, true
1104 field.update_attribute :multiple, true
1105 issue = Issue.find(1)
1105 issue = Issue.find(1)
1106 issue.custom_field_values = {1 => ['MySQL', 'Oracle']}
1106 issue.custom_field_values = {1 => ['MySQL', 'Oracle']}
1107 issue.save!
1107 issue.save!
1108
1108
1109 get :show, :id => 1
1109 get :show, :id => 1
1110 assert_response :success
1110 assert_response :success
1111
1111
1112 assert_tag :td, :content => 'MySQL, Oracle'
1112 assert_tag :td, :content => 'MySQL, Oracle'
1113 end
1113 end
1114
1114
1115 def test_show_with_multi_user_custom_field
1115 def test_show_with_multi_user_custom_field
1116 field = IssueCustomField.create!(:name => 'Multi user', :field_format => 'user', :multiple => true,
1116 field = IssueCustomField.create!(:name => 'Multi user', :field_format => 'user', :multiple => true,
1117 :tracker_ids => [1], :is_for_all => true)
1117 :tracker_ids => [1], :is_for_all => true)
1118 issue = Issue.find(1)
1118 issue = Issue.find(1)
1119 issue.custom_field_values = {field.id => ['2', '3']}
1119 issue.custom_field_values = {field.id => ['2', '3']}
1120 issue.save!
1120 issue.save!
1121
1121
1122 get :show, :id => 1
1122 get :show, :id => 1
1123 assert_response :success
1123 assert_response :success
1124
1124
1125 # TODO: should display links
1125 # TODO: should display links
1126 assert_tag :td, :content => 'Dave Lopper, John Smith'
1126 assert_tag :td, :content => 'Dave Lopper, John Smith'
1127 end
1127 end
1128
1128
1129 def test_show_atom
1129 def test_show_atom
1130 get :show, :id => 2, :format => 'atom'
1130 get :show, :id => 2, :format => 'atom'
1131 assert_response :success
1131 assert_response :success
1132 assert_template 'journals/index'
1132 assert_template 'journals/index'
1133 # Inline image
1133 # Inline image
1134 assert_select 'content', :text => Regexp.new(Regexp.quote('http://test.host/attachments/download/10'))
1134 assert_select 'content', :text => Regexp.new(Regexp.quote('http://test.host/attachments/download/10'))
1135 end
1135 end
1136
1136
1137 def test_show_export_to_pdf
1137 def test_show_export_to_pdf
1138 get :show, :id => 3, :format => 'pdf'
1138 get :show, :id => 3, :format => 'pdf'
1139 assert_response :success
1139 assert_response :success
1140 assert_equal 'application/pdf', @response.content_type
1140 assert_equal 'application/pdf', @response.content_type
1141 assert @response.body.starts_with?('%PDF')
1141 assert @response.body.starts_with?('%PDF')
1142 assert_not_nil assigns(:issue)
1142 assert_not_nil assigns(:issue)
1143 end
1143 end
1144
1144
1145 def test_get_new
1145 def test_get_new
1146 @request.session[:user_id] = 2
1146 @request.session[:user_id] = 2
1147 get :new, :project_id => 1, :tracker_id => 1
1147 get :new, :project_id => 1, :tracker_id => 1
1148 assert_response :success
1148 assert_response :success
1149 assert_template 'new'
1149 assert_template 'new'
1150
1150
1151 assert_tag 'input', :attributes => {:name => 'issue[is_private]'}
1151 assert_tag 'input', :attributes => {:name => 'issue[is_private]'}
1152 assert_no_tag 'select', :attributes => {:name => 'issue[project_id]'}
1152 assert_no_tag 'select', :attributes => {:name => 'issue[project_id]'}
1153 assert_tag 'select', :attributes => {:name => 'issue[tracker_id]'}
1153 assert_tag 'select', :attributes => {:name => 'issue[tracker_id]'}
1154 assert_tag 'input', :attributes => {:name => 'issue[subject]'}
1154 assert_tag 'input', :attributes => {:name => 'issue[subject]'}
1155 assert_tag 'textarea', :attributes => {:name => 'issue[description]'}
1155 assert_tag 'textarea', :attributes => {:name => 'issue[description]'}
1156 assert_tag 'select', :attributes => {:name => 'issue[status_id]'}
1156 assert_tag 'select', :attributes => {:name => 'issue[status_id]'}
1157 assert_tag 'select', :attributes => {:name => 'issue[priority_id]'}
1157 assert_tag 'select', :attributes => {:name => 'issue[priority_id]'}
1158 assert_tag 'select', :attributes => {:name => 'issue[assigned_to_id]'}
1158 assert_tag 'select', :attributes => {:name => 'issue[assigned_to_id]'}
1159 assert_tag 'select', :attributes => {:name => 'issue[category_id]'}
1159 assert_tag 'select', :attributes => {:name => 'issue[category_id]'}
1160 assert_tag 'select', :attributes => {:name => 'issue[fixed_version_id]'}
1160 assert_tag 'select', :attributes => {:name => 'issue[fixed_version_id]'}
1161 assert_tag 'input', :attributes => {:name => 'issue[parent_issue_id]'}
1161 assert_tag 'input', :attributes => {:name => 'issue[parent_issue_id]'}
1162 assert_tag 'input', :attributes => {:name => 'issue[start_date]'}
1162 assert_tag 'input', :attributes => {:name => 'issue[start_date]'}
1163 assert_tag 'input', :attributes => {:name => 'issue[due_date]'}
1163 assert_tag 'input', :attributes => {:name => 'issue[due_date]'}
1164 assert_tag 'select', :attributes => {:name => 'issue[done_ratio]'}
1164 assert_tag 'select', :attributes => {:name => 'issue[done_ratio]'}
1165 assert_tag 'input', :attributes => { :name => 'issue[custom_field_values][2]', :value => 'Default string' }
1165 assert_tag 'input', :attributes => { :name => 'issue[custom_field_values][2]', :value => 'Default string' }
1166 assert_tag 'input', :attributes => {:name => 'issue[watcher_user_ids][]'}
1166 assert_tag 'input', :attributes => {:name => 'issue[watcher_user_ids][]'}
1167
1167
1168 # Be sure we don't display inactive IssuePriorities
1168 # Be sure we don't display inactive IssuePriorities
1169 assert ! IssuePriority.find(15).active?
1169 assert ! IssuePriority.find(15).active?
1170 assert_no_tag :option, :attributes => {:value => '15'},
1170 assert_no_tag :option, :attributes => {:value => '15'},
1171 :parent => {:tag => 'select', :attributes => {:id => 'issue_priority_id'} }
1171 :parent => {:tag => 'select', :attributes => {:id => 'issue_priority_id'} }
1172 end
1172 end
1173
1173
1174 def test_get_new_with_minimal_permissions
1174 def test_get_new_with_minimal_permissions
1175 Role.find(1).update_attribute :permissions, [:add_issues]
1175 Role.find(1).update_attribute :permissions, [:add_issues]
1176 Workflow.delete_all :role_id => 1
1176 Workflow.delete_all :role_id => 1
1177
1177
1178 @request.session[:user_id] = 2
1178 @request.session[:user_id] = 2
1179 get :new, :project_id => 1, :tracker_id => 1
1179 get :new, :project_id => 1, :tracker_id => 1
1180 assert_response :success
1180 assert_response :success
1181 assert_template 'new'
1181 assert_template 'new'
1182
1182
1183 assert_no_tag 'input', :attributes => {:name => 'issue[is_private]'}
1183 assert_no_tag 'input', :attributes => {:name => 'issue[is_private]'}
1184 assert_no_tag 'select', :attributes => {:name => 'issue[project_id]'}
1184 assert_no_tag 'select', :attributes => {:name => 'issue[project_id]'}
1185 assert_tag 'select', :attributes => {:name => 'issue[tracker_id]'}
1185 assert_tag 'select', :attributes => {:name => 'issue[tracker_id]'}
1186 assert_tag 'input', :attributes => {:name => 'issue[subject]'}
1186 assert_tag 'input', :attributes => {:name => 'issue[subject]'}
1187 assert_tag 'textarea', :attributes => {:name => 'issue[description]'}
1187 assert_tag 'textarea', :attributes => {:name => 'issue[description]'}
1188 assert_tag 'select', :attributes => {:name => 'issue[status_id]'}
1188 assert_tag 'select', :attributes => {:name => 'issue[status_id]'}
1189 assert_tag 'select', :attributes => {:name => 'issue[priority_id]'}
1189 assert_tag 'select', :attributes => {:name => 'issue[priority_id]'}
1190 assert_tag 'select', :attributes => {:name => 'issue[assigned_to_id]'}
1190 assert_tag 'select', :attributes => {:name => 'issue[assigned_to_id]'}
1191 assert_tag 'select', :attributes => {:name => 'issue[category_id]'}
1191 assert_tag 'select', :attributes => {:name => 'issue[category_id]'}
1192 assert_tag 'select', :attributes => {:name => 'issue[fixed_version_id]'}
1192 assert_tag 'select', :attributes => {:name => 'issue[fixed_version_id]'}
1193 assert_no_tag 'input', :attributes => {:name => 'issue[parent_issue_id]'}
1193 assert_no_tag 'input', :attributes => {:name => 'issue[parent_issue_id]'}
1194 assert_tag 'input', :attributes => {:name => 'issue[start_date]'}
1194 assert_tag 'input', :attributes => {:name => 'issue[start_date]'}
1195 assert_tag 'input', :attributes => {:name => 'issue[due_date]'}
1195 assert_tag 'input', :attributes => {:name => 'issue[due_date]'}
1196 assert_tag 'select', :attributes => {:name => 'issue[done_ratio]'}
1196 assert_tag 'select', :attributes => {:name => 'issue[done_ratio]'}
1197 assert_tag 'input', :attributes => { :name => 'issue[custom_field_values][2]', :value => 'Default string' }
1197 assert_tag 'input', :attributes => { :name => 'issue[custom_field_values][2]', :value => 'Default string' }
1198 assert_no_tag 'input', :attributes => {:name => 'issue[watcher_user_ids][]'}
1198 assert_no_tag 'input', :attributes => {:name => 'issue[watcher_user_ids][]'}
1199 end
1199 end
1200
1200
1201 def test_get_new_with_multi_custom_field
1201 def test_get_new_with_multi_custom_field
1202 field = IssueCustomField.find(1)
1202 field = IssueCustomField.find(1)
1203 field.update_attribute :multiple, true
1203 field.update_attribute :multiple, true
1204
1204
1205 @request.session[:user_id] = 2
1205 @request.session[:user_id] = 2
1206 get :new, :project_id => 1, :tracker_id => 1
1206 get :new, :project_id => 1, :tracker_id => 1
1207 assert_response :success
1207 assert_response :success
1208 assert_template 'new'
1208 assert_template 'new'
1209
1209
1210 assert_tag 'select',
1210 assert_tag 'select',
1211 :attributes => {:name => 'issue[custom_field_values][1][]', :multiple => 'multiple'},
1211 :attributes => {:name => 'issue[custom_field_values][1][]', :multiple => 'multiple'},
1212 :children => {:count => 3},
1212 :children => {:count => 3},
1213 :child => {:tag => 'option', :attributes => {:value => 'MySQL'}, :content => 'MySQL'}
1213 :child => {:tag => 'option', :attributes => {:value => 'MySQL'}, :content => 'MySQL'}
1214 assert_tag 'input',
1214 assert_tag 'input',
1215 :attributes => {:name => 'issue[custom_field_values][1][]', :value => ''}
1215 :attributes => {:name => 'issue[custom_field_values][1][]', :value => ''}
1216 end
1216 end
1217
1217
1218 def test_get_new_with_multi_user_custom_field
1218 def test_get_new_with_multi_user_custom_field
1219 field = IssueCustomField.create!(:name => 'Multi user', :field_format => 'user', :multiple => true,
1219 field = IssueCustomField.create!(:name => 'Multi user', :field_format => 'user', :multiple => true,
1220 :tracker_ids => [1], :is_for_all => true)
1220 :tracker_ids => [1], :is_for_all => true)
1221
1221
1222 @request.session[:user_id] = 2
1222 @request.session[:user_id] = 2
1223 get :new, :project_id => 1, :tracker_id => 1
1223 get :new, :project_id => 1, :tracker_id => 1
1224 assert_response :success
1224 assert_response :success
1225 assert_template 'new'
1225 assert_template 'new'
1226
1226
1227 assert_tag 'select',
1227 assert_tag 'select',
1228 :attributes => {:name => "issue[custom_field_values][#{field.id}][]", :multiple => 'multiple'},
1228 :attributes => {:name => "issue[custom_field_values][#{field.id}][]", :multiple => 'multiple'},
1229 :children => {:count => Project.find(1).users.count},
1229 :children => {:count => Project.find(1).users.count},
1230 :child => {:tag => 'option', :attributes => {:value => '2'}, :content => 'John Smith'}
1230 :child => {:tag => 'option', :attributes => {:value => '2'}, :content => 'John Smith'}
1231 assert_tag 'input',
1231 assert_tag 'input',
1232 :attributes => {:name => "issue[custom_field_values][#{field.id}][]", :value => ''}
1232 :attributes => {:name => "issue[custom_field_values][#{field.id}][]", :value => ''}
1233 end
1233 end
1234
1234
1235 def test_get_new_without_default_start_date_is_creation_date
1235 def test_get_new_without_default_start_date_is_creation_date
1236 Setting.default_issue_start_date_to_creation_date = 0
1236 Setting.default_issue_start_date_to_creation_date = 0
1237
1237
1238 @request.session[:user_id] = 2
1238 @request.session[:user_id] = 2
1239 get :new, :project_id => 1, :tracker_id => 1
1239 get :new, :project_id => 1, :tracker_id => 1
1240 assert_response :success
1240 assert_response :success
1241 assert_template 'new'
1241 assert_template 'new'
1242
1242
1243 assert_tag :tag => 'input', :attributes => { :name => 'issue[start_date]',
1243 assert_tag :tag => 'input', :attributes => { :name => 'issue[start_date]',
1244 :value => nil }
1244 :value => nil }
1245 end
1245 end
1246
1246
1247 def test_get_new_with_default_start_date_is_creation_date
1247 def test_get_new_with_default_start_date_is_creation_date
1248 Setting.default_issue_start_date_to_creation_date = 1
1248 Setting.default_issue_start_date_to_creation_date = 1
1249
1249
1250 @request.session[:user_id] = 2
1250 @request.session[:user_id] = 2
1251 get :new, :project_id => 1, :tracker_id => 1
1251 get :new, :project_id => 1, :tracker_id => 1
1252 assert_response :success
1252 assert_response :success
1253 assert_template 'new'
1253 assert_template 'new'
1254
1254
1255 assert_tag :tag => 'input', :attributes => { :name => 'issue[start_date]',
1255 assert_tag :tag => 'input', :attributes => { :name => 'issue[start_date]',
1256 :value => Date.today.to_s }
1256 :value => Date.today.to_s }
1257 end
1257 end
1258
1258
1259 def test_get_new_form_should_allow_attachment_upload
1259 def test_get_new_form_should_allow_attachment_upload
1260 @request.session[:user_id] = 2
1260 @request.session[:user_id] = 2
1261 get :new, :project_id => 1, :tracker_id => 1
1261 get :new, :project_id => 1, :tracker_id => 1
1262
1262
1263 assert_tag :tag => 'form',
1263 assert_tag :tag => 'form',
1264 :attributes => {:id => 'issue-form', :method => 'post', :enctype => 'multipart/form-data'},
1264 :attributes => {:id => 'issue-form', :method => 'post', :enctype => 'multipart/form-data'},
1265 :descendant => {
1265 :descendant => {
1266 :tag => 'input',
1266 :tag => 'input',
1267 :attributes => {:type => 'file', :name => 'attachments[1][file]'}
1267 :attributes => {:type => 'file', :name => 'attachments[1][file]'}
1268 }
1268 }
1269 end
1269 end
1270
1270
1271 def test_get_new_without_tracker_id
1271 def test_get_new_without_tracker_id
1272 @request.session[:user_id] = 2
1272 @request.session[:user_id] = 2
1273 get :new, :project_id => 1
1273 get :new, :project_id => 1
1274 assert_response :success
1274 assert_response :success
1275 assert_template 'new'
1275 assert_template 'new'
1276
1276
1277 issue = assigns(:issue)
1277 issue = assigns(:issue)
1278 assert_not_nil issue
1278 assert_not_nil issue
1279 assert_equal Project.find(1).trackers.first, issue.tracker
1279 assert_equal Project.find(1).trackers.first, issue.tracker
1280 end
1280 end
1281
1281
1282 def test_get_new_with_no_default_status_should_display_an_error
1282 def test_get_new_with_no_default_status_should_display_an_error
1283 @request.session[:user_id] = 2
1283 @request.session[:user_id] = 2
1284 IssueStatus.delete_all
1284 IssueStatus.delete_all
1285
1285
1286 get :new, :project_id => 1
1286 get :new, :project_id => 1
1287 assert_response 500
1287 assert_response 500
1288 assert_error_tag :content => /No default issue/
1288 assert_error_tag :content => /No default issue/
1289 end
1289 end
1290
1290
1291 def test_get_new_with_no_tracker_should_display_an_error
1291 def test_get_new_with_no_tracker_should_display_an_error
1292 @request.session[:user_id] = 2
1292 @request.session[:user_id] = 2
1293 Tracker.delete_all
1293 Tracker.delete_all
1294
1294
1295 get :new, :project_id => 1
1295 get :new, :project_id => 1
1296 assert_response 500
1296 assert_response 500
1297 assert_error_tag :content => /No tracker/
1297 assert_error_tag :content => /No tracker/
1298 end
1298 end
1299
1299
1300 def test_update_new_form
1300 def test_update_new_form
1301 @request.session[:user_id] = 2
1301 @request.session[:user_id] = 2
1302 xhr :post, :new, :project_id => 1,
1302 xhr :post, :new, :project_id => 1,
1303 :issue => {:tracker_id => 2,
1303 :issue => {:tracker_id => 2,
1304 :subject => 'This is the test_new issue',
1304 :subject => 'This is the test_new issue',
1305 :description => 'This is the description',
1305 :description => 'This is the description',
1306 :priority_id => 5}
1306 :priority_id => 5}
1307 assert_response :success
1307 assert_response :success
1308 assert_template 'attributes'
1308 assert_template 'attributes'
1309
1309
1310 issue = assigns(:issue)
1310 issue = assigns(:issue)
1311 assert_kind_of Issue, issue
1311 assert_kind_of Issue, issue
1312 assert_equal 1, issue.project_id
1312 assert_equal 1, issue.project_id
1313 assert_equal 2, issue.tracker_id
1313 assert_equal 2, issue.tracker_id
1314 assert_equal 'This is the test_new issue', issue.subject
1314 assert_equal 'This is the test_new issue', issue.subject
1315 end
1315 end
1316
1316
1317 def test_post_create
1317 def test_post_create
1318 @request.session[:user_id] = 2
1318 @request.session[:user_id] = 2
1319 assert_difference 'Issue.count' do
1319 assert_difference 'Issue.count' do
1320 post :create, :project_id => 1,
1320 post :create, :project_id => 1,
1321 :issue => {:tracker_id => 3,
1321 :issue => {:tracker_id => 3,
1322 :status_id => 2,
1322 :status_id => 2,
1323 :subject => 'This is the test_new issue',
1323 :subject => 'This is the test_new issue',
1324 :description => 'This is the description',
1324 :description => 'This is the description',
1325 :priority_id => 5,
1325 :priority_id => 5,
1326 :start_date => '2010-11-07',
1326 :start_date => '2010-11-07',
1327 :estimated_hours => '',
1327 :estimated_hours => '',
1328 :custom_field_values => {'2' => 'Value for field 2'}}
1328 :custom_field_values => {'2' => 'Value for field 2'}}
1329 end
1329 end
1330 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
1330 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
1331
1331
1332 issue = Issue.find_by_subject('This is the test_new issue')
1332 issue = Issue.find_by_subject('This is the test_new issue')
1333 assert_not_nil issue
1333 assert_not_nil issue
1334 assert_equal 2, issue.author_id
1334 assert_equal 2, issue.author_id
1335 assert_equal 3, issue.tracker_id
1335 assert_equal 3, issue.tracker_id
1336 assert_equal 2, issue.status_id
1336 assert_equal 2, issue.status_id
1337 assert_equal Date.parse('2010-11-07'), issue.start_date
1337 assert_equal Date.parse('2010-11-07'), issue.start_date
1338 assert_nil issue.estimated_hours
1338 assert_nil issue.estimated_hours
1339 v = issue.custom_values.find(:first, :conditions => {:custom_field_id => 2})
1339 v = issue.custom_values.find(:first, :conditions => {:custom_field_id => 2})
1340 assert_not_nil v
1340 assert_not_nil v
1341 assert_equal 'Value for field 2', v.value
1341 assert_equal 'Value for field 2', v.value
1342 end
1342 end
1343
1343
1344 def test_post_new_with_group_assignment
1344 def test_post_new_with_group_assignment
1345 group = Group.find(11)
1345 group = Group.find(11)
1346 project = Project.find(1)
1346 project = Project.find(1)
1347 project.members << Member.new(:principal => group, :roles => [Role.first])
1347 project.members << Member.new(:principal => group, :roles => [Role.first])
1348
1348
1349 with_settings :issue_group_assignment => '1' do
1349 with_settings :issue_group_assignment => '1' do
1350 @request.session[:user_id] = 2
1350 @request.session[:user_id] = 2
1351 assert_difference 'Issue.count' do
1351 assert_difference 'Issue.count' do
1352 post :create, :project_id => project.id,
1352 post :create, :project_id => project.id,
1353 :issue => {:tracker_id => 3,
1353 :issue => {:tracker_id => 3,
1354 :status_id => 1,
1354 :status_id => 1,
1355 :subject => 'This is the test_new_with_group_assignment issue',
1355 :subject => 'This is the test_new_with_group_assignment issue',
1356 :assigned_to_id => group.id}
1356 :assigned_to_id => group.id}
1357 end
1357 end
1358 end
1358 end
1359 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
1359 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
1360
1360
1361 issue = Issue.find_by_subject('This is the test_new_with_group_assignment issue')
1361 issue = Issue.find_by_subject('This is the test_new_with_group_assignment issue')
1362 assert_not_nil issue
1362 assert_not_nil issue
1363 assert_equal group, issue.assigned_to
1363 assert_equal group, issue.assigned_to
1364 end
1364 end
1365
1365
1366 def test_post_create_without_start_date_and_default_start_date_is_not_creation_date
1366 def test_post_create_without_start_date_and_default_start_date_is_not_creation_date
1367 Setting.default_issue_start_date_to_creation_date = 0
1367 Setting.default_issue_start_date_to_creation_date = 0
1368
1368
1369 @request.session[:user_id] = 2
1369 @request.session[:user_id] = 2
1370 assert_difference 'Issue.count' do
1370 assert_difference 'Issue.count' do
1371 post :create, :project_id => 1,
1371 post :create, :project_id => 1,
1372 :issue => {:tracker_id => 3,
1372 :issue => {:tracker_id => 3,
1373 :status_id => 2,
1373 :status_id => 2,
1374 :subject => 'This is the test_new issue',
1374 :subject => 'This is the test_new issue',
1375 :description => 'This is the description',
1375 :description => 'This is the description',
1376 :priority_id => 5,
1376 :priority_id => 5,
1377 :estimated_hours => '',
1377 :estimated_hours => '',
1378 :custom_field_values => {'2' => 'Value for field 2'}}
1378 :custom_field_values => {'2' => 'Value for field 2'}}
1379 end
1379 end
1380 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
1380 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
1381
1381
1382 issue = Issue.find_by_subject('This is the test_new issue')
1382 issue = Issue.find_by_subject('This is the test_new issue')
1383 assert_not_nil issue
1383 assert_not_nil issue
1384 assert_nil issue.start_date
1384 assert_nil issue.start_date
1385 end
1385 end
1386
1386
1387 def test_post_create_without_start_date_and_default_start_date_is_creation_date
1387 def test_post_create_without_start_date_and_default_start_date_is_creation_date
1388 Setting.default_issue_start_date_to_creation_date = 1
1388 Setting.default_issue_start_date_to_creation_date = 1
1389
1389
1390 @request.session[:user_id] = 2
1390 @request.session[:user_id] = 2
1391 assert_difference 'Issue.count' do
1391 assert_difference 'Issue.count' do
1392 post :create, :project_id => 1,
1392 post :create, :project_id => 1,
1393 :issue => {:tracker_id => 3,
1393 :issue => {:tracker_id => 3,
1394 :status_id => 2,
1394 :status_id => 2,
1395 :subject => 'This is the test_new issue',
1395 :subject => 'This is the test_new issue',
1396 :description => 'This is the description',
1396 :description => 'This is the description',
1397 :priority_id => 5,
1397 :priority_id => 5,
1398 :estimated_hours => '',
1398 :estimated_hours => '',
1399 :custom_field_values => {'2' => 'Value for field 2'}}
1399 :custom_field_values => {'2' => 'Value for field 2'}}
1400 end
1400 end
1401 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
1401 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
1402
1402
1403 issue = Issue.find_by_subject('This is the test_new issue')
1403 issue = Issue.find_by_subject('This is the test_new issue')
1404 assert_not_nil issue
1404 assert_not_nil issue
1405 assert_equal Date.today, issue.start_date
1405 assert_equal Date.today, issue.start_date
1406 end
1406 end
1407
1407
1408 def test_post_create_and_continue
1408 def test_post_create_and_continue
1409 @request.session[:user_id] = 2
1409 @request.session[:user_id] = 2
1410 assert_difference 'Issue.count' do
1410 assert_difference 'Issue.count' do
1411 post :create, :project_id => 1,
1411 post :create, :project_id => 1,
1412 :issue => {:tracker_id => 3, :subject => 'This is first issue', :priority_id => 5},
1412 :issue => {:tracker_id => 3, :subject => 'This is first issue', :priority_id => 5},
1413 :continue => ''
1413 :continue => ''
1414 end
1414 end
1415
1415
1416 issue = Issue.first(:order => 'id DESC')
1416 issue = Issue.first(:order => 'id DESC')
1417 assert_redirected_to :controller => 'issues', :action => 'new', :project_id => 'ecookbook', :issue => {:tracker_id => 3}
1417 assert_redirected_to :controller => 'issues', :action => 'new', :project_id => 'ecookbook', :issue => {:tracker_id => 3}
1418 assert_not_nil flash[:notice], "flash was not set"
1418 assert_not_nil flash[:notice], "flash was not set"
1419 assert flash[:notice].include?("<a href='/issues/#{issue.id}'>##{issue.id}</a>"), "issue link not found in flash: #{flash[:notice]}"
1419 assert flash[:notice].include?("<a href='/issues/#{issue.id}'>##{issue.id}</a>"), "issue link not found in flash: #{flash[:notice]}"
1420 end
1420 end
1421
1421
1422 def test_post_create_without_custom_fields_param
1422 def test_post_create_without_custom_fields_param
1423 @request.session[:user_id] = 2
1423 @request.session[:user_id] = 2
1424 assert_difference 'Issue.count' do
1424 assert_difference 'Issue.count' do
1425 post :create, :project_id => 1,
1425 post :create, :project_id => 1,
1426 :issue => {:tracker_id => 1,
1426 :issue => {:tracker_id => 1,
1427 :subject => 'This is the test_new issue',
1427 :subject => 'This is the test_new issue',
1428 :description => 'This is the description',
1428 :description => 'This is the description',
1429 :priority_id => 5}
1429 :priority_id => 5}
1430 end
1430 end
1431 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
1431 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
1432 end
1432 end
1433
1433
1434 def test_post_create_with_multi_custom_field
1434 def test_post_create_with_multi_custom_field
1435 field = IssueCustomField.find_by_name('Database')
1435 field = IssueCustomField.find_by_name('Database')
1436 field.update_attribute(:multiple, true)
1436 field.update_attribute(:multiple, true)
1437
1437
1438 @request.session[:user_id] = 2
1438 @request.session[:user_id] = 2
1439 assert_difference 'Issue.count' do
1439 assert_difference 'Issue.count' do
1440 post :create, :project_id => 1,
1440 post :create, :project_id => 1,
1441 :issue => {:tracker_id => 1,
1441 :issue => {:tracker_id => 1,
1442 :subject => 'This is the test_new issue',
1442 :subject => 'This is the test_new issue',
1443 :description => 'This is the description',
1443 :description => 'This is the description',
1444 :priority_id => 5,
1444 :priority_id => 5,
1445 :custom_field_values => {'1' => ['', 'MySQL', 'Oracle']}}
1445 :custom_field_values => {'1' => ['', 'MySQL', 'Oracle']}}
1446 end
1446 end
1447 assert_response 302
1447 assert_response 302
1448 issue = Issue.first(:order => 'id DESC')
1448 issue = Issue.first(:order => 'id DESC')
1449 assert_equal ['MySQL', 'Oracle'], issue.custom_field_value(1).sort
1449 assert_equal ['MySQL', 'Oracle'], issue.custom_field_value(1).sort
1450 end
1450 end
1451
1451
1452 def test_post_create_with_empty_multi_custom_field
1452 def test_post_create_with_empty_multi_custom_field
1453 field = IssueCustomField.find_by_name('Database')
1453 field = IssueCustomField.find_by_name('Database')
1454 field.update_attribute(:multiple, true)
1454 field.update_attribute(:multiple, true)
1455
1455
1456 @request.session[:user_id] = 2
1456 @request.session[:user_id] = 2
1457 assert_difference 'Issue.count' do
1457 assert_difference 'Issue.count' do
1458 post :create, :project_id => 1,
1458 post :create, :project_id => 1,
1459 :issue => {:tracker_id => 1,
1459 :issue => {:tracker_id => 1,
1460 :subject => 'This is the test_new issue',
1460 :subject => 'This is the test_new issue',
1461 :description => 'This is the description',
1461 :description => 'This is the description',
1462 :priority_id => 5,
1462 :priority_id => 5,
1463 :custom_field_values => {'1' => ['']}}
1463 :custom_field_values => {'1' => ['']}}
1464 end
1464 end
1465 assert_response 302
1465 assert_response 302
1466 issue = Issue.first(:order => 'id DESC')
1466 issue = Issue.first(:order => 'id DESC')
1467 assert_equal [''], issue.custom_field_value(1).sort
1467 assert_equal [''], issue.custom_field_value(1).sort
1468 end
1468 end
1469
1469
1470 def test_post_create_with_multi_user_custom_field
1470 def test_post_create_with_multi_user_custom_field
1471 field = IssueCustomField.create!(:name => 'Multi user', :field_format => 'user', :multiple => true,
1471 field = IssueCustomField.create!(:name => 'Multi user', :field_format => 'user', :multiple => true,
1472 :tracker_ids => [1], :is_for_all => true)
1472 :tracker_ids => [1], :is_for_all => true)
1473
1473
1474 @request.session[:user_id] = 2
1474 @request.session[:user_id] = 2
1475 assert_difference 'Issue.count' do
1475 assert_difference 'Issue.count' do
1476 post :create, :project_id => 1,
1476 post :create, :project_id => 1,
1477 :issue => {:tracker_id => 1,
1477 :issue => {:tracker_id => 1,
1478 :subject => 'This is the test_new issue',
1478 :subject => 'This is the test_new issue',
1479 :description => 'This is the description',
1479 :description => 'This is the description',
1480 :priority_id => 5,
1480 :priority_id => 5,
1481 :custom_field_values => {field.id.to_s => ['', '2', '3']}}
1481 :custom_field_values => {field.id.to_s => ['', '2', '3']}}
1482 end
1482 end
1483 assert_response 302
1483 assert_response 302
1484 issue = Issue.first(:order => 'id DESC')
1484 issue = Issue.first(:order => 'id DESC')
1485 assert_equal ['2', '3'], issue.custom_field_value(field).sort
1485 assert_equal ['2', '3'], issue.custom_field_value(field).sort
1486 end
1486 end
1487
1487
1488 def test_post_create_with_required_custom_field_and_without_custom_fields_param
1488 def test_post_create_with_required_custom_field_and_without_custom_fields_param
1489 field = IssueCustomField.find_by_name('Database')
1489 field = IssueCustomField.find_by_name('Database')
1490 field.update_attribute(:is_required, true)
1490 field.update_attribute(:is_required, true)
1491
1491
1492 @request.session[:user_id] = 2
1492 @request.session[:user_id] = 2
1493 assert_no_difference 'Issue.count' do
1493 assert_no_difference 'Issue.count' do
1494 post :create, :project_id => 1,
1494 post :create, :project_id => 1,
1495 :issue => {:tracker_id => 1,
1495 :issue => {:tracker_id => 1,
1496 :subject => 'This is the test_new issue',
1496 :subject => 'This is the test_new issue',
1497 :description => 'This is the description',
1497 :description => 'This is the description',
1498 :priority_id => 5}
1498 :priority_id => 5}
1499 end
1499 end
1500 assert_response :success
1500 assert_response :success
1501 assert_template 'new'
1501 assert_template 'new'
1502 issue = assigns(:issue)
1502 issue = assigns(:issue)
1503 assert_not_nil issue
1503 assert_not_nil issue
1504 assert_error_tag :content => /Database can't be blank/
1504 assert_error_tag :content => /Database can't be blank/
1505 end
1505 end
1506
1506
1507 def test_post_create_with_watchers
1507 def test_post_create_with_watchers
1508 @request.session[:user_id] = 2
1508 @request.session[:user_id] = 2
1509 ActionMailer::Base.deliveries.clear
1509 ActionMailer::Base.deliveries.clear
1510
1510
1511 assert_difference 'Watcher.count', 2 do
1511 assert_difference 'Watcher.count', 2 do
1512 post :create, :project_id => 1,
1512 post :create, :project_id => 1,
1513 :issue => {:tracker_id => 1,
1513 :issue => {:tracker_id => 1,
1514 :subject => 'This is a new issue with watchers',
1514 :subject => 'This is a new issue with watchers',
1515 :description => 'This is the description',
1515 :description => 'This is the description',
1516 :priority_id => 5,
1516 :priority_id => 5,
1517 :watcher_user_ids => ['2', '3']}
1517 :watcher_user_ids => ['2', '3']}
1518 end
1518 end
1519 issue = Issue.find_by_subject('This is a new issue with watchers')
1519 issue = Issue.find_by_subject('This is a new issue with watchers')
1520 assert_not_nil issue
1520 assert_not_nil issue
1521 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
1521 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
1522
1522
1523 # Watchers added
1523 # Watchers added
1524 assert_equal [2, 3], issue.watcher_user_ids.sort
1524 assert_equal [2, 3], issue.watcher_user_ids.sort
1525 assert issue.watched_by?(User.find(3))
1525 assert issue.watched_by?(User.find(3))
1526 # Watchers notified
1526 # Watchers notified
1527 mail = ActionMailer::Base.deliveries.last
1527 mail = ActionMailer::Base.deliveries.last
1528 assert_kind_of TMail::Mail, mail
1528 assert_kind_of TMail::Mail, mail
1529 assert [mail.bcc, mail.cc].flatten.include?(User.find(3).mail)
1529 assert [mail.bcc, mail.cc].flatten.include?(User.find(3).mail)
1530 end
1530 end
1531
1531
1532 def test_post_create_subissue
1532 def test_post_create_subissue
1533 @request.session[:user_id] = 2
1533 @request.session[:user_id] = 2
1534
1534
1535 assert_difference 'Issue.count' do
1535 assert_difference 'Issue.count' do
1536 post :create, :project_id => 1,
1536 post :create, :project_id => 1,
1537 :issue => {:tracker_id => 1,
1537 :issue => {:tracker_id => 1,
1538 :subject => 'This is a child issue',
1538 :subject => 'This is a child issue',
1539 :parent_issue_id => 2}
1539 :parent_issue_id => 2}
1540 end
1540 end
1541 issue = Issue.find_by_subject('This is a child issue')
1541 issue = Issue.find_by_subject('This is a child issue')
1542 assert_not_nil issue
1542 assert_not_nil issue
1543 assert_equal Issue.find(2), issue.parent
1543 assert_equal Issue.find(2), issue.parent
1544 end
1544 end
1545
1545
1546 def test_post_create_subissue_with_non_numeric_parent_id
1546 def test_post_create_subissue_with_non_numeric_parent_id
1547 @request.session[:user_id] = 2
1547 @request.session[:user_id] = 2
1548
1548
1549 assert_difference 'Issue.count' do
1549 assert_difference 'Issue.count' do
1550 post :create, :project_id => 1,
1550 post :create, :project_id => 1,
1551 :issue => {:tracker_id => 1,
1551 :issue => {:tracker_id => 1,
1552 :subject => 'This is a child issue',
1552 :subject => 'This is a child issue',
1553 :parent_issue_id => 'ABC'}
1553 :parent_issue_id => 'ABC'}
1554 end
1554 end
1555 issue = Issue.find_by_subject('This is a child issue')
1555 issue = Issue.find_by_subject('This is a child issue')
1556 assert_not_nil issue
1556 assert_not_nil issue
1557 assert_nil issue.parent
1557 assert_nil issue.parent
1558 end
1558 end
1559
1559
1560 def test_post_create_private
1560 def test_post_create_private
1561 @request.session[:user_id] = 2
1561 @request.session[:user_id] = 2
1562
1562
1563 assert_difference 'Issue.count' do
1563 assert_difference 'Issue.count' do
1564 post :create, :project_id => 1,
1564 post :create, :project_id => 1,
1565 :issue => {:tracker_id => 1,
1565 :issue => {:tracker_id => 1,
1566 :subject => 'This is a private issue',
1566 :subject => 'This is a private issue',
1567 :is_private => '1'}
1567 :is_private => '1'}
1568 end
1568 end
1569 issue = Issue.first(:order => 'id DESC')
1569 issue = Issue.first(:order => 'id DESC')
1570 assert issue.is_private?
1570 assert issue.is_private?
1571 end
1571 end
1572
1572
1573 def test_post_create_private_with_set_own_issues_private_permission
1573 def test_post_create_private_with_set_own_issues_private_permission
1574 role = Role.find(1)
1574 role = Role.find(1)
1575 role.remove_permission! :set_issues_private
1575 role.remove_permission! :set_issues_private
1576 role.add_permission! :set_own_issues_private
1576 role.add_permission! :set_own_issues_private
1577
1577
1578 @request.session[:user_id] = 2
1578 @request.session[:user_id] = 2
1579
1579
1580 assert_difference 'Issue.count' do
1580 assert_difference 'Issue.count' do
1581 post :create, :project_id => 1,
1581 post :create, :project_id => 1,
1582 :issue => {:tracker_id => 1,
1582 :issue => {:tracker_id => 1,
1583 :subject => 'This is a private issue',
1583 :subject => 'This is a private issue',
1584 :is_private => '1'}
1584 :is_private => '1'}
1585 end
1585 end
1586 issue = Issue.first(:order => 'id DESC')
1586 issue = Issue.first(:order => 'id DESC')
1587 assert issue.is_private?
1587 assert issue.is_private?
1588 end
1588 end
1589
1589
1590 def test_post_create_should_send_a_notification
1590 def test_post_create_should_send_a_notification
1591 ActionMailer::Base.deliveries.clear
1591 ActionMailer::Base.deliveries.clear
1592 @request.session[:user_id] = 2
1592 @request.session[:user_id] = 2
1593 assert_difference 'Issue.count' do
1593 assert_difference 'Issue.count' do
1594 post :create, :project_id => 1,
1594 post :create, :project_id => 1,
1595 :issue => {:tracker_id => 3,
1595 :issue => {:tracker_id => 3,
1596 :subject => 'This is the test_new issue',
1596 :subject => 'This is the test_new issue',
1597 :description => 'This is the description',
1597 :description => 'This is the description',
1598 :priority_id => 5,
1598 :priority_id => 5,
1599 :estimated_hours => '',
1599 :estimated_hours => '',
1600 :custom_field_values => {'2' => 'Value for field 2'}}
1600 :custom_field_values => {'2' => 'Value for field 2'}}
1601 end
1601 end
1602 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
1602 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
1603
1603
1604 assert_equal 1, ActionMailer::Base.deliveries.size
1604 assert_equal 1, ActionMailer::Base.deliveries.size
1605 end
1605 end
1606
1606
1607 def test_post_create_should_preserve_fields_values_on_validation_failure
1607 def test_post_create_should_preserve_fields_values_on_validation_failure
1608 @request.session[:user_id] = 2
1608 @request.session[:user_id] = 2
1609 post :create, :project_id => 1,
1609 post :create, :project_id => 1,
1610 :issue => {:tracker_id => 1,
1610 :issue => {:tracker_id => 1,
1611 # empty subject
1611 # empty subject
1612 :subject => '',
1612 :subject => '',
1613 :description => 'This is a description',
1613 :description => 'This is a description',
1614 :priority_id => 6,
1614 :priority_id => 6,
1615 :custom_field_values => {'1' => 'Oracle', '2' => 'Value for field 2'}}
1615 :custom_field_values => {'1' => 'Oracle', '2' => 'Value for field 2'}}
1616 assert_response :success
1616 assert_response :success
1617 assert_template 'new'
1617 assert_template 'new'
1618
1618
1619 assert_tag :textarea, :attributes => { :name => 'issue[description]' },
1619 assert_tag :textarea, :attributes => { :name => 'issue[description]' },
1620 :content => 'This is a description'
1620 :content => 'This is a description'
1621 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
1621 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
1622 :child => { :tag => 'option', :attributes => { :selected => 'selected',
1622 :child => { :tag => 'option', :attributes => { :selected => 'selected',
1623 :value => '6' },
1623 :value => '6' },
1624 :content => 'High' }
1624 :content => 'High' }
1625 # Custom fields
1625 # Custom fields
1626 assert_tag :select, :attributes => { :name => 'issue[custom_field_values][1]' },
1626 assert_tag :select, :attributes => { :name => 'issue[custom_field_values][1]' },
1627 :child => { :tag => 'option', :attributes => { :selected => 'selected',
1627 :child => { :tag => 'option', :attributes => { :selected => 'selected',
1628 :value => 'Oracle' },
1628 :value => 'Oracle' },
1629 :content => 'Oracle' }
1629 :content => 'Oracle' }
1630 assert_tag :input, :attributes => { :name => 'issue[custom_field_values][2]',
1630 assert_tag :input, :attributes => { :name => 'issue[custom_field_values][2]',
1631 :value => 'Value for field 2'}
1631 :value => 'Value for field 2'}
1632 end
1632 end
1633
1633
1634 def test_post_create_should_ignore_non_safe_attributes
1634 def test_post_create_should_ignore_non_safe_attributes
1635 @request.session[:user_id] = 2
1635 @request.session[:user_id] = 2
1636 assert_nothing_raised do
1636 assert_nothing_raised do
1637 post :create, :project_id => 1, :issue => { :tracker => "A param can not be a Tracker" }
1637 post :create, :project_id => 1, :issue => { :tracker => "A param can not be a Tracker" }
1638 end
1638 end
1639 end
1639 end
1640
1640
1641 def test_post_create_with_attachment
1641 def test_post_create_with_attachment
1642 set_tmp_attachments_directory
1642 set_tmp_attachments_directory
1643 @request.session[:user_id] = 2
1643 @request.session[:user_id] = 2
1644
1644
1645 assert_difference 'Issue.count' do
1645 assert_difference 'Issue.count' do
1646 assert_difference 'Attachment.count' do
1646 assert_difference 'Attachment.count' do
1647 post :create, :project_id => 1,
1647 post :create, :project_id => 1,
1648 :issue => { :tracker_id => '1', :subject => 'With attachment' },
1648 :issue => { :tracker_id => '1', :subject => 'With attachment' },
1649 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'test file'}}
1649 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'test file'}}
1650 end
1650 end
1651 end
1651 end
1652
1652
1653 issue = Issue.first(:order => 'id DESC')
1653 issue = Issue.first(:order => 'id DESC')
1654 attachment = Attachment.first(:order => 'id DESC')
1654 attachment = Attachment.first(:order => 'id DESC')
1655
1655
1656 assert_equal issue, attachment.container
1656 assert_equal issue, attachment.container
1657 assert_equal 2, attachment.author_id
1657 assert_equal 2, attachment.author_id
1658 assert_equal 'testfile.txt', attachment.filename
1658 assert_equal 'testfile.txt', attachment.filename
1659 assert_equal 'text/plain', attachment.content_type
1659 assert_equal 'text/plain', attachment.content_type
1660 assert_equal 'test file', attachment.description
1660 assert_equal 'test file', attachment.description
1661 assert_equal 59, attachment.filesize
1661 assert_equal 59, attachment.filesize
1662 assert File.exists?(attachment.diskfile)
1662 assert File.exists?(attachment.diskfile)
1663 assert_equal 59, File.size(attachment.diskfile)
1663 assert_equal 59, File.size(attachment.diskfile)
1664 end
1664 end
1665
1665
1666 def test_post_create_with_failure_should_save_attachments
1667 set_tmp_attachments_directory
1668 @request.session[:user_id] = 2
1669
1670 assert_no_difference 'Issue.count' do
1671 assert_difference 'Attachment.count' do
1672 post :create, :project_id => 1,
1673 :issue => { :tracker_id => '1', :subject => '' },
1674 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'test file'}}
1675 assert_response :success
1676 assert_template 'new'
1677 end
1678 end
1679
1680 attachment = Attachment.first(:order => 'id DESC')
1681 assert_equal 'testfile.txt', attachment.filename
1682 assert File.exists?(attachment.diskfile)
1683 assert_nil attachment.container
1684
1685 assert_tag 'input', :attributes => {:name => 'attachments[p0][token]', :value => attachment.token}
1686 assert_tag 'span', :content => /testfile.txt/
1687 end
1688
1689 def test_post_create_with_failure_should_keep_saved_attachments
1690 set_tmp_attachments_directory
1691 attachment = Attachment.create!(:file => uploaded_test_file("testfile.txt", "text/plain"), :author_id => 2)
1692 @request.session[:user_id] = 2
1693
1694 assert_no_difference 'Issue.count' do
1695 assert_no_difference 'Attachment.count' do
1696 post :create, :project_id => 1,
1697 :issue => { :tracker_id => '1', :subject => '' },
1698 :attachments => {'p0' => {'token' => attachment.token}}
1699 assert_response :success
1700 assert_template 'new'
1701 end
1702 end
1703
1704 assert_tag 'input', :attributes => {:name => 'attachments[p0][token]', :value => attachment.token}
1705 assert_tag 'span', :content => /testfile.txt/
1706 end
1707
1708 def test_post_create_should_attach_saved_attachments
1709 set_tmp_attachments_directory
1710 attachment = Attachment.create!(:file => uploaded_test_file("testfile.txt", "text/plain"), :author_id => 2)
1711 @request.session[:user_id] = 2
1712
1713 assert_difference 'Issue.count' do
1714 assert_no_difference 'Attachment.count' do
1715 post :create, :project_id => 1,
1716 :issue => { :tracker_id => '1', :subject => 'Saved attachments' },
1717 :attachments => {'p0' => {'token' => attachment.token}}
1718 assert_response 302
1719 end
1720 end
1721
1722 issue = Issue.first(:order => 'id DESC')
1723 assert_equal 1, issue.attachments.count
1724
1725 attachment.reload
1726 assert_equal issue, attachment.container
1727 end
1728
1666 context "without workflow privilege" do
1729 context "without workflow privilege" do
1667 setup do
1730 setup do
1668 Workflow.delete_all(["role_id = ?", Role.anonymous.id])
1731 Workflow.delete_all(["role_id = ?", Role.anonymous.id])
1669 Role.anonymous.add_permission! :add_issues, :add_issue_notes
1732 Role.anonymous.add_permission! :add_issues, :add_issue_notes
1670 end
1733 end
1671
1734
1672 context "#new" do
1735 context "#new" do
1673 should "propose default status only" do
1736 should "propose default status only" do
1674 get :new, :project_id => 1
1737 get :new, :project_id => 1
1675 assert_response :success
1738 assert_response :success
1676 assert_template 'new'
1739 assert_template 'new'
1677 assert_tag :tag => 'select',
1740 assert_tag :tag => 'select',
1678 :attributes => {:name => 'issue[status_id]'},
1741 :attributes => {:name => 'issue[status_id]'},
1679 :children => {:count => 1},
1742 :children => {:count => 1},
1680 :child => {:tag => 'option', :attributes => {:value => IssueStatus.default.id.to_s}}
1743 :child => {:tag => 'option', :attributes => {:value => IssueStatus.default.id.to_s}}
1681 end
1744 end
1682
1745
1683 should "accept default status" do
1746 should "accept default status" do
1684 assert_difference 'Issue.count' do
1747 assert_difference 'Issue.count' do
1685 post :create, :project_id => 1,
1748 post :create, :project_id => 1,
1686 :issue => {:tracker_id => 1,
1749 :issue => {:tracker_id => 1,
1687 :subject => 'This is an issue',
1750 :subject => 'This is an issue',
1688 :status_id => 1}
1751 :status_id => 1}
1689 end
1752 end
1690 issue = Issue.last(:order => 'id')
1753 issue = Issue.last(:order => 'id')
1691 assert_equal IssueStatus.default, issue.status
1754 assert_equal IssueStatus.default, issue.status
1692 end
1755 end
1693
1756
1694 should "ignore unauthorized status" do
1757 should "ignore unauthorized status" do
1695 assert_difference 'Issue.count' do
1758 assert_difference 'Issue.count' do
1696 post :create, :project_id => 1,
1759 post :create, :project_id => 1,
1697 :issue => {:tracker_id => 1,
1760 :issue => {:tracker_id => 1,
1698 :subject => 'This is an issue',
1761 :subject => 'This is an issue',
1699 :status_id => 3}
1762 :status_id => 3}
1700 end
1763 end
1701 issue = Issue.last(:order => 'id')
1764 issue = Issue.last(:order => 'id')
1702 assert_equal IssueStatus.default, issue.status
1765 assert_equal IssueStatus.default, issue.status
1703 end
1766 end
1704 end
1767 end
1705
1768
1706 context "#update" do
1769 context "#update" do
1707 should "ignore status change" do
1770 should "ignore status change" do
1708 assert_difference 'Journal.count' do
1771 assert_difference 'Journal.count' do
1709 put :update, :id => 1, :notes => 'just trying', :issue => {:status_id => 3}
1772 put :update, :id => 1, :notes => 'just trying', :issue => {:status_id => 3}
1710 end
1773 end
1711 assert_equal 1, Issue.find(1).status_id
1774 assert_equal 1, Issue.find(1).status_id
1712 end
1775 end
1713
1776
1714 should "ignore attributes changes" do
1777 should "ignore attributes changes" do
1715 assert_difference 'Journal.count' do
1778 assert_difference 'Journal.count' do
1716 put :update, :id => 1, :notes => 'just trying', :issue => {:subject => 'changed', :assigned_to_id => 2}
1779 put :update, :id => 1, :notes => 'just trying', :issue => {:subject => 'changed', :assigned_to_id => 2}
1717 end
1780 end
1718 issue = Issue.find(1)
1781 issue = Issue.find(1)
1719 assert_equal "Can't print recipes", issue.subject
1782 assert_equal "Can't print recipes", issue.subject
1720 assert_nil issue.assigned_to
1783 assert_nil issue.assigned_to
1721 end
1784 end
1722 end
1785 end
1723 end
1786 end
1724
1787
1725 context "with workflow privilege" do
1788 context "with workflow privilege" do
1726 setup do
1789 setup do
1727 Workflow.delete_all(["role_id = ?", Role.anonymous.id])
1790 Workflow.delete_all(["role_id = ?", Role.anonymous.id])
1728 Workflow.create!(:role => Role.anonymous, :tracker_id => 1, :old_status_id => 1, :new_status_id => 3)
1791 Workflow.create!(:role => Role.anonymous, :tracker_id => 1, :old_status_id => 1, :new_status_id => 3)
1729 Workflow.create!(:role => Role.anonymous, :tracker_id => 1, :old_status_id => 1, :new_status_id => 4)
1792 Workflow.create!(:role => Role.anonymous, :tracker_id => 1, :old_status_id => 1, :new_status_id => 4)
1730 Role.anonymous.add_permission! :add_issues, :add_issue_notes
1793 Role.anonymous.add_permission! :add_issues, :add_issue_notes
1731 end
1794 end
1732
1795
1733 context "#update" do
1796 context "#update" do
1734 should "accept authorized status" do
1797 should "accept authorized status" do
1735 assert_difference 'Journal.count' do
1798 assert_difference 'Journal.count' do
1736 put :update, :id => 1, :notes => 'just trying', :issue => {:status_id => 3}
1799 put :update, :id => 1, :notes => 'just trying', :issue => {:status_id => 3}
1737 end
1800 end
1738 assert_equal 3, Issue.find(1).status_id
1801 assert_equal 3, Issue.find(1).status_id
1739 end
1802 end
1740
1803
1741 should "ignore unauthorized status" do
1804 should "ignore unauthorized status" do
1742 assert_difference 'Journal.count' do
1805 assert_difference 'Journal.count' do
1743 put :update, :id => 1, :notes => 'just trying', :issue => {:status_id => 2}
1806 put :update, :id => 1, :notes => 'just trying', :issue => {:status_id => 2}
1744 end
1807 end
1745 assert_equal 1, Issue.find(1).status_id
1808 assert_equal 1, Issue.find(1).status_id
1746 end
1809 end
1747
1810
1748 should "accept authorized attributes changes" do
1811 should "accept authorized attributes changes" do
1749 assert_difference 'Journal.count' do
1812 assert_difference 'Journal.count' do
1750 put :update, :id => 1, :notes => 'just trying', :issue => {:assigned_to_id => 2}
1813 put :update, :id => 1, :notes => 'just trying', :issue => {:assigned_to_id => 2}
1751 end
1814 end
1752 issue = Issue.find(1)
1815 issue = Issue.find(1)
1753 assert_equal 2, issue.assigned_to_id
1816 assert_equal 2, issue.assigned_to_id
1754 end
1817 end
1755
1818
1756 should "ignore unauthorized attributes changes" do
1819 should "ignore unauthorized attributes changes" do
1757 assert_difference 'Journal.count' do
1820 assert_difference 'Journal.count' do
1758 put :update, :id => 1, :notes => 'just trying', :issue => {:subject => 'changed'}
1821 put :update, :id => 1, :notes => 'just trying', :issue => {:subject => 'changed'}
1759 end
1822 end
1760 issue = Issue.find(1)
1823 issue = Issue.find(1)
1761 assert_equal "Can't print recipes", issue.subject
1824 assert_equal "Can't print recipes", issue.subject
1762 end
1825 end
1763 end
1826 end
1764
1827
1765 context "and :edit_issues permission" do
1828 context "and :edit_issues permission" do
1766 setup do
1829 setup do
1767 Role.anonymous.add_permission! :add_issues, :edit_issues
1830 Role.anonymous.add_permission! :add_issues, :edit_issues
1768 end
1831 end
1769
1832
1770 should "accept authorized status" do
1833 should "accept authorized status" do
1771 assert_difference 'Journal.count' do
1834 assert_difference 'Journal.count' do
1772 put :update, :id => 1, :notes => 'just trying', :issue => {:status_id => 3}
1835 put :update, :id => 1, :notes => 'just trying', :issue => {:status_id => 3}
1773 end
1836 end
1774 assert_equal 3, Issue.find(1).status_id
1837 assert_equal 3, Issue.find(1).status_id
1775 end
1838 end
1776
1839
1777 should "ignore unauthorized status" do
1840 should "ignore unauthorized status" do
1778 assert_difference 'Journal.count' do
1841 assert_difference 'Journal.count' do
1779 put :update, :id => 1, :notes => 'just trying', :issue => {:status_id => 2}
1842 put :update, :id => 1, :notes => 'just trying', :issue => {:status_id => 2}
1780 end
1843 end
1781 assert_equal 1, Issue.find(1).status_id
1844 assert_equal 1, Issue.find(1).status_id
1782 end
1845 end
1783
1846
1784 should "accept authorized attributes changes" do
1847 should "accept authorized attributes changes" do
1785 assert_difference 'Journal.count' do
1848 assert_difference 'Journal.count' do
1786 put :update, :id => 1, :notes => 'just trying', :issue => {:subject => 'changed', :assigned_to_id => 2}
1849 put :update, :id => 1, :notes => 'just trying', :issue => {:subject => 'changed', :assigned_to_id => 2}
1787 end
1850 end
1788 issue = Issue.find(1)
1851 issue = Issue.find(1)
1789 assert_equal "changed", issue.subject
1852 assert_equal "changed", issue.subject
1790 assert_equal 2, issue.assigned_to_id
1853 assert_equal 2, issue.assigned_to_id
1791 end
1854 end
1792 end
1855 end
1793 end
1856 end
1794
1857
1795 def test_new_as_copy
1858 def test_new_as_copy
1796 @request.session[:user_id] = 2
1859 @request.session[:user_id] = 2
1797 get :new, :project_id => 1, :copy_from => 1
1860 get :new, :project_id => 1, :copy_from => 1
1798
1861
1799 assert_response :success
1862 assert_response :success
1800 assert_template 'new'
1863 assert_template 'new'
1801
1864
1802 assert_not_nil assigns(:issue)
1865 assert_not_nil assigns(:issue)
1803 orig = Issue.find(1)
1866 orig = Issue.find(1)
1804 assert_equal 1, assigns(:issue).project_id
1867 assert_equal 1, assigns(:issue).project_id
1805 assert_equal orig.subject, assigns(:issue).subject
1868 assert_equal orig.subject, assigns(:issue).subject
1806 assert assigns(:issue).copy?
1869 assert assigns(:issue).copy?
1807
1870
1808 assert_tag 'form', :attributes => {:id => 'issue-form', :action => '/projects/ecookbook/issues'}
1871 assert_tag 'form', :attributes => {:id => 'issue-form', :action => '/projects/ecookbook/issues'}
1809 assert_tag 'select', :attributes => {:name => 'issue[project_id]'}
1872 assert_tag 'select', :attributes => {:name => 'issue[project_id]'}
1810 assert_tag 'select', :attributes => {:name => 'issue[project_id]'},
1873 assert_tag 'select', :attributes => {:name => 'issue[project_id]'},
1811 :child => {:tag => 'option', :attributes => {:value => '1', :selected => 'selected'}, :content => 'eCookbook'}
1874 :child => {:tag => 'option', :attributes => {:value => '1', :selected => 'selected'}, :content => 'eCookbook'}
1812 assert_tag 'select', :attributes => {:name => 'issue[project_id]'},
1875 assert_tag 'select', :attributes => {:name => 'issue[project_id]'},
1813 :child => {:tag => 'option', :attributes => {:value => '2', :selected => nil}, :content => 'OnlineStore'}
1876 :child => {:tag => 'option', :attributes => {:value => '2', :selected => nil}, :content => 'OnlineStore'}
1814 assert_tag 'input', :attributes => {:name => 'copy_from', :value => '1'}
1877 assert_tag 'input', :attributes => {:name => 'copy_from', :value => '1'}
1815 end
1878 end
1816
1879
1817 def test_new_as_copy_with_attachments_should_show_copy_attachments_checkbox
1880 def test_new_as_copy_with_attachments_should_show_copy_attachments_checkbox
1818 @request.session[:user_id] = 2
1881 @request.session[:user_id] = 2
1819 issue = Issue.find(3)
1882 issue = Issue.find(3)
1820 assert issue.attachments.count > 0
1883 assert issue.attachments.count > 0
1821 get :new, :project_id => 1, :copy_from => 3
1884 get :new, :project_id => 1, :copy_from => 3
1822
1885
1823 assert_tag 'input', :attributes => {:name => 'copy_attachments', :type => 'checkbox', :checked => 'checked', :value => '1'}
1886 assert_tag 'input', :attributes => {:name => 'copy_attachments', :type => 'checkbox', :checked => 'checked', :value => '1'}
1824 end
1887 end
1825
1888
1826 def test_new_as_copy_without_attachments_should_not_show_copy_attachments_checkbox
1889 def test_new_as_copy_without_attachments_should_not_show_copy_attachments_checkbox
1827 @request.session[:user_id] = 2
1890 @request.session[:user_id] = 2
1828 issue = Issue.find(3)
1891 issue = Issue.find(3)
1829 issue.attachments.delete_all
1892 issue.attachments.delete_all
1830 get :new, :project_id => 1, :copy_from => 3
1893 get :new, :project_id => 1, :copy_from => 3
1831
1894
1832 assert_no_tag 'input', :attributes => {:name => 'copy_attachments', :type => 'checkbox', :checked => 'checked', :value => '1'}
1895 assert_no_tag 'input', :attributes => {:name => 'copy_attachments', :type => 'checkbox', :checked => 'checked', :value => '1'}
1833 end
1896 end
1834
1897
1835 def test_new_as_copy_with_invalid_issue_should_respond_with_404
1898 def test_new_as_copy_with_invalid_issue_should_respond_with_404
1836 @request.session[:user_id] = 2
1899 @request.session[:user_id] = 2
1837 get :new, :project_id => 1, :copy_from => 99999
1900 get :new, :project_id => 1, :copy_from => 99999
1838 assert_response 404
1901 assert_response 404
1839 end
1902 end
1840
1903
1841 def test_create_as_copy_on_different_project
1904 def test_create_as_copy_on_different_project
1842 @request.session[:user_id] = 2
1905 @request.session[:user_id] = 2
1843 assert_difference 'Issue.count' do
1906 assert_difference 'Issue.count' do
1844 post :create, :project_id => 1, :copy_from => 1,
1907 post :create, :project_id => 1, :copy_from => 1,
1845 :issue => {:project_id => '2', :tracker_id => '3', :status_id => '1', :subject => 'Copy'}
1908 :issue => {:project_id => '2', :tracker_id => '3', :status_id => '1', :subject => 'Copy'}
1846
1909
1847 assert_not_nil assigns(:issue)
1910 assert_not_nil assigns(:issue)
1848 assert assigns(:issue).copy?
1911 assert assigns(:issue).copy?
1849 end
1912 end
1850 issue = Issue.first(:order => 'id DESC')
1913 issue = Issue.first(:order => 'id DESC')
1851 assert_redirected_to "/issues/#{issue.id}"
1914 assert_redirected_to "/issues/#{issue.id}"
1852
1915
1853 assert_equal 2, issue.project_id
1916 assert_equal 2, issue.project_id
1854 assert_equal 3, issue.tracker_id
1917 assert_equal 3, issue.tracker_id
1855 assert_equal 'Copy', issue.subject
1918 assert_equal 'Copy', issue.subject
1856 end
1919 end
1857
1920
1858 def test_create_as_copy_should_copy_attachments
1921 def test_create_as_copy_should_copy_attachments
1859 @request.session[:user_id] = 2
1922 @request.session[:user_id] = 2
1860 issue = Issue.find(3)
1923 issue = Issue.find(3)
1861 count = issue.attachments.count
1924 count = issue.attachments.count
1862 assert count > 0
1925 assert count > 0
1863
1926
1864 assert_difference 'Issue.count' do
1927 assert_difference 'Issue.count' do
1865 assert_difference 'Attachment.count', count do
1928 assert_difference 'Attachment.count', count do
1866 assert_no_difference 'Journal.count' do
1929 assert_no_difference 'Journal.count' do
1867 post :create, :project_id => 1, :copy_from => 3,
1930 post :create, :project_id => 1, :copy_from => 3,
1868 :issue => {:project_id => '1', :tracker_id => '3', :status_id => '1', :subject => 'Copy with attachments'},
1931 :issue => {:project_id => '1', :tracker_id => '3', :status_id => '1', :subject => 'Copy with attachments'},
1869 :copy_attachments => '1'
1932 :copy_attachments => '1'
1870 end
1933 end
1871 end
1934 end
1872 end
1935 end
1873 copy = Issue.first(:order => 'id DESC')
1936 copy = Issue.first(:order => 'id DESC')
1874 assert_equal count, copy.attachments.count
1937 assert_equal count, copy.attachments.count
1875 assert_equal issue.attachments.map(&:filename).sort, copy.attachments.map(&:filename).sort
1938 assert_equal issue.attachments.map(&:filename).sort, copy.attachments.map(&:filename).sort
1876 end
1939 end
1877
1940
1878 def test_create_as_copy_without_copy_attachments_option_should_not_copy_attachments
1941 def test_create_as_copy_without_copy_attachments_option_should_not_copy_attachments
1879 @request.session[:user_id] = 2
1942 @request.session[:user_id] = 2
1880 issue = Issue.find(3)
1943 issue = Issue.find(3)
1881 count = issue.attachments.count
1944 count = issue.attachments.count
1882 assert count > 0
1945 assert count > 0
1883
1946
1884 assert_difference 'Issue.count' do
1947 assert_difference 'Issue.count' do
1885 assert_no_difference 'Attachment.count' do
1948 assert_no_difference 'Attachment.count' do
1886 assert_no_difference 'Journal.count' do
1949 assert_no_difference 'Journal.count' do
1887 post :create, :project_id => 1, :copy_from => 3,
1950 post :create, :project_id => 1, :copy_from => 3,
1888 :issue => {:project_id => '1', :tracker_id => '3', :status_id => '1', :subject => 'Copy with attachments'}
1951 :issue => {:project_id => '1', :tracker_id => '3', :status_id => '1', :subject => 'Copy with attachments'}
1889 end
1952 end
1890 end
1953 end
1891 end
1954 end
1892 copy = Issue.first(:order => 'id DESC')
1955 copy = Issue.first(:order => 'id DESC')
1893 assert_equal 0, copy.attachments.count
1956 assert_equal 0, copy.attachments.count
1894 end
1957 end
1895
1958
1896 def test_create_as_copy_with_attachments_should_add_new_files
1959 def test_create_as_copy_with_attachments_should_add_new_files
1897 @request.session[:user_id] = 2
1960 @request.session[:user_id] = 2
1898 issue = Issue.find(3)
1961 issue = Issue.find(3)
1899 count = issue.attachments.count
1962 count = issue.attachments.count
1900 assert count > 0
1963 assert count > 0
1901
1964
1902 assert_difference 'Issue.count' do
1965 assert_difference 'Issue.count' do
1903 assert_difference 'Attachment.count', count + 1 do
1966 assert_difference 'Attachment.count', count + 1 do
1904 assert_no_difference 'Journal.count' do
1967 assert_no_difference 'Journal.count' do
1905 post :create, :project_id => 1, :copy_from => 3,
1968 post :create, :project_id => 1, :copy_from => 3,
1906 :issue => {:project_id => '1', :tracker_id => '3', :status_id => '1', :subject => 'Copy with attachments'},
1969 :issue => {:project_id => '1', :tracker_id => '3', :status_id => '1', :subject => 'Copy with attachments'},
1907 :copy_attachments => '1',
1970 :copy_attachments => '1',
1908 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'test file'}}
1971 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'test file'}}
1909 end
1972 end
1910 end
1973 end
1911 end
1974 end
1912 copy = Issue.first(:order => 'id DESC')
1975 copy = Issue.first(:order => 'id DESC')
1913 assert_equal count + 1, copy.attachments.count
1976 assert_equal count + 1, copy.attachments.count
1914 end
1977 end
1915
1978
1916 def test_create_as_copy_with_failure
1979 def test_create_as_copy_with_failure
1917 @request.session[:user_id] = 2
1980 @request.session[:user_id] = 2
1918 post :create, :project_id => 1, :copy_from => 1,
1981 post :create, :project_id => 1, :copy_from => 1,
1919 :issue => {:project_id => '2', :tracker_id => '3', :status_id => '1', :subject => ''}
1982 :issue => {:project_id => '2', :tracker_id => '3', :status_id => '1', :subject => ''}
1920
1983
1921 assert_response :success
1984 assert_response :success
1922 assert_template 'new'
1985 assert_template 'new'
1923
1986
1924 assert_not_nil assigns(:issue)
1987 assert_not_nil assigns(:issue)
1925 assert assigns(:issue).copy?
1988 assert assigns(:issue).copy?
1926
1989
1927 assert_tag 'form', :attributes => {:id => 'issue-form', :action => '/projects/ecookbook/issues'}
1990 assert_tag 'form', :attributes => {:id => 'issue-form', :action => '/projects/ecookbook/issues'}
1928 assert_tag 'select', :attributes => {:name => 'issue[project_id]'}
1991 assert_tag 'select', :attributes => {:name => 'issue[project_id]'}
1929 assert_tag 'select', :attributes => {:name => 'issue[project_id]'},
1992 assert_tag 'select', :attributes => {:name => 'issue[project_id]'},
1930 :child => {:tag => 'option', :attributes => {:value => '1', :selected => nil}, :content => 'eCookbook'}
1993 :child => {:tag => 'option', :attributes => {:value => '1', :selected => nil}, :content => 'eCookbook'}
1931 assert_tag 'select', :attributes => {:name => 'issue[project_id]'},
1994 assert_tag 'select', :attributes => {:name => 'issue[project_id]'},
1932 :child => {:tag => 'option', :attributes => {:value => '2', :selected => 'selected'}, :content => 'OnlineStore'}
1995 :child => {:tag => 'option', :attributes => {:value => '2', :selected => 'selected'}, :content => 'OnlineStore'}
1933 assert_tag 'input', :attributes => {:name => 'copy_from', :value => '1'}
1996 assert_tag 'input', :attributes => {:name => 'copy_from', :value => '1'}
1934 end
1997 end
1935
1998
1936 def test_create_as_copy_on_project_without_permission_should_ignore_target_project
1999 def test_create_as_copy_on_project_without_permission_should_ignore_target_project
1937 @request.session[:user_id] = 2
2000 @request.session[:user_id] = 2
1938 assert !User.find(2).member_of?(Project.find(4))
2001 assert !User.find(2).member_of?(Project.find(4))
1939
2002
1940 assert_difference 'Issue.count' do
2003 assert_difference 'Issue.count' do
1941 post :create, :project_id => 1, :copy_from => 1,
2004 post :create, :project_id => 1, :copy_from => 1,
1942 :issue => {:project_id => '4', :tracker_id => '3', :status_id => '1', :subject => 'Copy'}
2005 :issue => {:project_id => '4', :tracker_id => '3', :status_id => '1', :subject => 'Copy'}
1943 end
2006 end
1944 issue = Issue.first(:order => 'id DESC')
2007 issue = Issue.first(:order => 'id DESC')
1945 assert_equal 1, issue.project_id
2008 assert_equal 1, issue.project_id
1946 end
2009 end
1947
2010
1948 def test_get_edit
2011 def test_get_edit
1949 @request.session[:user_id] = 2
2012 @request.session[:user_id] = 2
1950 get :edit, :id => 1
2013 get :edit, :id => 1
1951 assert_response :success
2014 assert_response :success
1952 assert_template 'edit'
2015 assert_template 'edit'
1953 assert_not_nil assigns(:issue)
2016 assert_not_nil assigns(:issue)
1954 assert_equal Issue.find(1), assigns(:issue)
2017 assert_equal Issue.find(1), assigns(:issue)
1955
2018
1956 # Be sure we don't display inactive IssuePriorities
2019 # Be sure we don't display inactive IssuePriorities
1957 assert ! IssuePriority.find(15).active?
2020 assert ! IssuePriority.find(15).active?
1958 assert_no_tag :option, :attributes => {:value => '15'},
2021 assert_no_tag :option, :attributes => {:value => '15'},
1959 :parent => {:tag => 'select', :attributes => {:id => 'issue_priority_id'} }
2022 :parent => {:tag => 'select', :attributes => {:id => 'issue_priority_id'} }
1960 end
2023 end
1961
2024
1962 def test_get_edit_should_display_the_time_entry_form_with_log_time_permission
2025 def test_get_edit_should_display_the_time_entry_form_with_log_time_permission
1963 @request.session[:user_id] = 2
2026 @request.session[:user_id] = 2
1964 Role.find_by_name('Manager').update_attribute :permissions, [:view_issues, :edit_issues, :log_time]
2027 Role.find_by_name('Manager').update_attribute :permissions, [:view_issues, :edit_issues, :log_time]
1965
2028
1966 get :edit, :id => 1
2029 get :edit, :id => 1
1967 assert_tag 'input', :attributes => {:name => 'time_entry[hours]'}
2030 assert_tag 'input', :attributes => {:name => 'time_entry[hours]'}
1968 end
2031 end
1969
2032
1970 def test_get_edit_should_not_display_the_time_entry_form_without_log_time_permission
2033 def test_get_edit_should_not_display_the_time_entry_form_without_log_time_permission
1971 @request.session[:user_id] = 2
2034 @request.session[:user_id] = 2
1972 Role.find_by_name('Manager').remove_permission! :log_time
2035 Role.find_by_name('Manager').remove_permission! :log_time
1973
2036
1974 get :edit, :id => 1
2037 get :edit, :id => 1
1975 assert_no_tag 'input', :attributes => {:name => 'time_entry[hours]'}
2038 assert_no_tag 'input', :attributes => {:name => 'time_entry[hours]'}
1976 end
2039 end
1977
2040
1978 def test_get_edit_with_params
2041 def test_get_edit_with_params
1979 @request.session[:user_id] = 2
2042 @request.session[:user_id] = 2
1980 get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 },
2043 get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 },
1981 :time_entry => { :hours => '2.5', :comments => 'test_get_edit_with_params', :activity_id => TimeEntryActivity.first.id }
2044 :time_entry => { :hours => '2.5', :comments => 'test_get_edit_with_params', :activity_id => TimeEntryActivity.first.id }
1982 assert_response :success
2045 assert_response :success
1983 assert_template 'edit'
2046 assert_template 'edit'
1984
2047
1985 issue = assigns(:issue)
2048 issue = assigns(:issue)
1986 assert_not_nil issue
2049 assert_not_nil issue
1987
2050
1988 assert_equal 5, issue.status_id
2051 assert_equal 5, issue.status_id
1989 assert_tag :select, :attributes => { :name => 'issue[status_id]' },
2052 assert_tag :select, :attributes => { :name => 'issue[status_id]' },
1990 :child => { :tag => 'option',
2053 :child => { :tag => 'option',
1991 :content => 'Closed',
2054 :content => 'Closed',
1992 :attributes => { :selected => 'selected' } }
2055 :attributes => { :selected => 'selected' } }
1993
2056
1994 assert_equal 7, issue.priority_id
2057 assert_equal 7, issue.priority_id
1995 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
2058 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
1996 :child => { :tag => 'option',
2059 :child => { :tag => 'option',
1997 :content => 'Urgent',
2060 :content => 'Urgent',
1998 :attributes => { :selected => 'selected' } }
2061 :attributes => { :selected => 'selected' } }
1999
2062
2000 assert_tag :input, :attributes => { :name => 'time_entry[hours]', :value => '2.5' }
2063 assert_tag :input, :attributes => { :name => 'time_entry[hours]', :value => '2.5' }
2001 assert_tag :select, :attributes => { :name => 'time_entry[activity_id]' },
2064 assert_tag :select, :attributes => { :name => 'time_entry[activity_id]' },
2002 :child => { :tag => 'option',
2065 :child => { :tag => 'option',
2003 :attributes => { :selected => 'selected', :value => TimeEntryActivity.first.id } }
2066 :attributes => { :selected => 'selected', :value => TimeEntryActivity.first.id } }
2004 assert_tag :input, :attributes => { :name => 'time_entry[comments]', :value => 'test_get_edit_with_params' }
2067 assert_tag :input, :attributes => { :name => 'time_entry[comments]', :value => 'test_get_edit_with_params' }
2005 end
2068 end
2006
2069
2007 def test_get_edit_with_multi_custom_field
2070 def test_get_edit_with_multi_custom_field
2008 field = CustomField.find(1)
2071 field = CustomField.find(1)
2009 field.update_attribute :multiple, true
2072 field.update_attribute :multiple, true
2010 issue = Issue.find(1)
2073 issue = Issue.find(1)
2011 issue.custom_field_values = {1 => ['MySQL', 'Oracle']}
2074 issue.custom_field_values = {1 => ['MySQL', 'Oracle']}
2012 issue.save!
2075 issue.save!
2013
2076
2014 @request.session[:user_id] = 2
2077 @request.session[:user_id] = 2
2015 get :edit, :id => 1
2078 get :edit, :id => 1
2016 assert_response :success
2079 assert_response :success
2017 assert_template 'edit'
2080 assert_template 'edit'
2018
2081
2019 assert_tag 'select', :attributes => {:name => 'issue[custom_field_values][1][]', :multiple => 'multiple'}
2082 assert_tag 'select', :attributes => {:name => 'issue[custom_field_values][1][]', :multiple => 'multiple'}
2020 assert_tag 'select', :attributes => {:name => 'issue[custom_field_values][1][]'},
2083 assert_tag 'select', :attributes => {:name => 'issue[custom_field_values][1][]'},
2021 :child => {:tag => 'option', :attributes => {:value => 'MySQL', :selected => 'selected'}}
2084 :child => {:tag => 'option', :attributes => {:value => 'MySQL', :selected => 'selected'}}
2022 assert_tag 'select', :attributes => {:name => 'issue[custom_field_values][1][]'},
2085 assert_tag 'select', :attributes => {:name => 'issue[custom_field_values][1][]'},
2023 :child => {:tag => 'option', :attributes => {:value => 'PostgreSQL', :selected => nil}}
2086 :child => {:tag => 'option', :attributes => {:value => 'PostgreSQL', :selected => nil}}
2024 assert_tag 'select', :attributes => {:name => 'issue[custom_field_values][1][]'},
2087 assert_tag 'select', :attributes => {:name => 'issue[custom_field_values][1][]'},
2025 :child => {:tag => 'option', :attributes => {:value => 'Oracle', :selected => 'selected'}}
2088 :child => {:tag => 'option', :attributes => {:value => 'Oracle', :selected => 'selected'}}
2026 end
2089 end
2027
2090
2028 def test_update_edit_form
2091 def test_update_edit_form
2029 @request.session[:user_id] = 2
2092 @request.session[:user_id] = 2
2030 xhr :put, :new, :project_id => 1,
2093 xhr :put, :new, :project_id => 1,
2031 :id => 1,
2094 :id => 1,
2032 :issue => {:tracker_id => 2,
2095 :issue => {:tracker_id => 2,
2033 :subject => 'This is the test_new issue',
2096 :subject => 'This is the test_new issue',
2034 :description => 'This is the description',
2097 :description => 'This is the description',
2035 :priority_id => 5}
2098 :priority_id => 5}
2036 assert_response :success
2099 assert_response :success
2037 assert_template 'attributes'
2100 assert_template 'attributes'
2038
2101
2039 issue = assigns(:issue)
2102 issue = assigns(:issue)
2040 assert_kind_of Issue, issue
2103 assert_kind_of Issue, issue
2041 assert_equal 1, issue.id
2104 assert_equal 1, issue.id
2042 assert_equal 1, issue.project_id
2105 assert_equal 1, issue.project_id
2043 assert_equal 2, issue.tracker_id
2106 assert_equal 2, issue.tracker_id
2044 assert_equal 'This is the test_new issue', issue.subject
2107 assert_equal 'This is the test_new issue', issue.subject
2045 end
2108 end
2046
2109
2047 def test_update_edit_form_with_project_change
2110 def test_update_edit_form_with_project_change
2048 @request.session[:user_id] = 2
2111 @request.session[:user_id] = 2
2049 xhr :put, :new, :project_id => 1,
2112 xhr :put, :new, :project_id => 1,
2050 :id => 1,
2113 :id => 1,
2051 :project_change => '1',
2114 :project_change => '1',
2052 :issue => {:project_id => 2,
2115 :issue => {:project_id => 2,
2053 :tracker_id => 2,
2116 :tracker_id => 2,
2054 :subject => 'This is the test_new issue',
2117 :subject => 'This is the test_new issue',
2055 :description => 'This is the description',
2118 :description => 'This is the description',
2056 :priority_id => 5}
2119 :priority_id => 5}
2057 assert_response :success
2120 assert_response :success
2058 assert_template 'form'
2121 assert_template 'form'
2059
2122
2060 issue = assigns(:issue)
2123 issue = assigns(:issue)
2061 assert_kind_of Issue, issue
2124 assert_kind_of Issue, issue
2062 assert_equal 1, issue.id
2125 assert_equal 1, issue.id
2063 assert_equal 2, issue.project_id
2126 assert_equal 2, issue.project_id
2064 assert_equal 2, issue.tracker_id
2127 assert_equal 2, issue.tracker_id
2065 assert_equal 'This is the test_new issue', issue.subject
2128 assert_equal 'This is the test_new issue', issue.subject
2066 end
2129 end
2067
2130
2068 def test_update_using_invalid_http_verbs
2131 def test_update_using_invalid_http_verbs
2069 @request.session[:user_id] = 2
2132 @request.session[:user_id] = 2
2070 subject = 'Updated by an invalid http verb'
2133 subject = 'Updated by an invalid http verb'
2071
2134
2072 get :update, :id => 1, :issue => {:subject => subject}
2135 get :update, :id => 1, :issue => {:subject => subject}
2073 assert_not_equal subject, Issue.find(1).subject
2136 assert_not_equal subject, Issue.find(1).subject
2074
2137
2075 post :update, :id => 1, :issue => {:subject => subject}
2138 post :update, :id => 1, :issue => {:subject => subject}
2076 assert_not_equal subject, Issue.find(1).subject
2139 assert_not_equal subject, Issue.find(1).subject
2077
2140
2078 delete :update, :id => 1, :issue => {:subject => subject}
2141 delete :update, :id => 1, :issue => {:subject => subject}
2079 assert_not_equal subject, Issue.find(1).subject
2142 assert_not_equal subject, Issue.find(1).subject
2080 end
2143 end
2081
2144
2082 def test_put_update_without_custom_fields_param
2145 def test_put_update_without_custom_fields_param
2083 @request.session[:user_id] = 2
2146 @request.session[:user_id] = 2
2084 ActionMailer::Base.deliveries.clear
2147 ActionMailer::Base.deliveries.clear
2085
2148
2086 issue = Issue.find(1)
2149 issue = Issue.find(1)
2087 assert_equal '125', issue.custom_value_for(2).value
2150 assert_equal '125', issue.custom_value_for(2).value
2088 old_subject = issue.subject
2151 old_subject = issue.subject
2089 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
2152 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
2090
2153
2091 assert_difference('Journal.count') do
2154 assert_difference('Journal.count') do
2092 assert_difference('JournalDetail.count', 2) do
2155 assert_difference('JournalDetail.count', 2) do
2093 put :update, :id => 1, :issue => {:subject => new_subject,
2156 put :update, :id => 1, :issue => {:subject => new_subject,
2094 :priority_id => '6',
2157 :priority_id => '6',
2095 :category_id => '1' # no change
2158 :category_id => '1' # no change
2096 }
2159 }
2097 end
2160 end
2098 end
2161 end
2099 assert_redirected_to :action => 'show', :id => '1'
2162 assert_redirected_to :action => 'show', :id => '1'
2100 issue.reload
2163 issue.reload
2101 assert_equal new_subject, issue.subject
2164 assert_equal new_subject, issue.subject
2102 # Make sure custom fields were not cleared
2165 # Make sure custom fields were not cleared
2103 assert_equal '125', issue.custom_value_for(2).value
2166 assert_equal '125', issue.custom_value_for(2).value
2104
2167
2105 mail = ActionMailer::Base.deliveries.last
2168 mail = ActionMailer::Base.deliveries.last
2106 assert_kind_of TMail::Mail, mail
2169 assert_kind_of TMail::Mail, mail
2107 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
2170 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
2108 assert mail.body.include?("Subject changed from #{old_subject} to #{new_subject}")
2171 assert mail.body.include?("Subject changed from #{old_subject} to #{new_subject}")
2109 end
2172 end
2110
2173
2111 def test_put_update_with_project_change
2174 def test_put_update_with_project_change
2112 @request.session[:user_id] = 2
2175 @request.session[:user_id] = 2
2113 ActionMailer::Base.deliveries.clear
2176 ActionMailer::Base.deliveries.clear
2114
2177
2115 assert_difference('Journal.count') do
2178 assert_difference('Journal.count') do
2116 assert_difference('JournalDetail.count', 3) do
2179 assert_difference('JournalDetail.count', 3) do
2117 put :update, :id => 1, :issue => {:project_id => '2',
2180 put :update, :id => 1, :issue => {:project_id => '2',
2118 :tracker_id => '1', # no change
2181 :tracker_id => '1', # no change
2119 :priority_id => '6',
2182 :priority_id => '6',
2120 :category_id => '3'
2183 :category_id => '3'
2121 }
2184 }
2122 end
2185 end
2123 end
2186 end
2124 assert_redirected_to :action => 'show', :id => '1'
2187 assert_redirected_to :action => 'show', :id => '1'
2125 issue = Issue.find(1)
2188 issue = Issue.find(1)
2126 assert_equal 2, issue.project_id
2189 assert_equal 2, issue.project_id
2127 assert_equal 1, issue.tracker_id
2190 assert_equal 1, issue.tracker_id
2128 assert_equal 6, issue.priority_id
2191 assert_equal 6, issue.priority_id
2129 assert_equal 3, issue.category_id
2192 assert_equal 3, issue.category_id
2130
2193
2131 mail = ActionMailer::Base.deliveries.last
2194 mail = ActionMailer::Base.deliveries.last
2132 assert_not_nil mail
2195 assert_not_nil mail
2133 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
2196 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
2134 assert mail.body.include?("Project changed from eCookbook to OnlineStore")
2197 assert mail.body.include?("Project changed from eCookbook to OnlineStore")
2135 end
2198 end
2136
2199
2137 def test_put_update_with_tracker_change
2200 def test_put_update_with_tracker_change
2138 @request.session[:user_id] = 2
2201 @request.session[:user_id] = 2
2139 ActionMailer::Base.deliveries.clear
2202 ActionMailer::Base.deliveries.clear
2140
2203
2141 assert_difference('Journal.count') do
2204 assert_difference('Journal.count') do
2142 assert_difference('JournalDetail.count', 2) do
2205 assert_difference('JournalDetail.count', 2) do
2143 put :update, :id => 1, :issue => {:project_id => '1',
2206 put :update, :id => 1, :issue => {:project_id => '1',
2144 :tracker_id => '2',
2207 :tracker_id => '2',
2145 :priority_id => '6'
2208 :priority_id => '6'
2146 }
2209 }
2147 end
2210 end
2148 end
2211 end
2149 assert_redirected_to :action => 'show', :id => '1'
2212 assert_redirected_to :action => 'show', :id => '1'
2150 issue = Issue.find(1)
2213 issue = Issue.find(1)
2151 assert_equal 1, issue.project_id
2214 assert_equal 1, issue.project_id
2152 assert_equal 2, issue.tracker_id
2215 assert_equal 2, issue.tracker_id
2153 assert_equal 6, issue.priority_id
2216 assert_equal 6, issue.priority_id
2154 assert_equal 1, issue.category_id
2217 assert_equal 1, issue.category_id
2155
2218
2156 mail = ActionMailer::Base.deliveries.last
2219 mail = ActionMailer::Base.deliveries.last
2157 assert_not_nil mail
2220 assert_not_nil mail
2158 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
2221 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
2159 assert mail.body.include?("Tracker changed from Bug to Feature request")
2222 assert mail.body.include?("Tracker changed from Bug to Feature request")
2160 end
2223 end
2161
2224
2162 def test_put_update_with_custom_field_change
2225 def test_put_update_with_custom_field_change
2163 @request.session[:user_id] = 2
2226 @request.session[:user_id] = 2
2164 issue = Issue.find(1)
2227 issue = Issue.find(1)
2165 assert_equal '125', issue.custom_value_for(2).value
2228 assert_equal '125', issue.custom_value_for(2).value
2166
2229
2167 assert_difference('Journal.count') do
2230 assert_difference('Journal.count') do
2168 assert_difference('JournalDetail.count', 3) do
2231 assert_difference('JournalDetail.count', 3) do
2169 put :update, :id => 1, :issue => {:subject => 'Custom field change',
2232 put :update, :id => 1, :issue => {:subject => 'Custom field change',
2170 :priority_id => '6',
2233 :priority_id => '6',
2171 :category_id => '1', # no change
2234 :category_id => '1', # no change
2172 :custom_field_values => { '2' => 'New custom value' }
2235 :custom_field_values => { '2' => 'New custom value' }
2173 }
2236 }
2174 end
2237 end
2175 end
2238 end
2176 assert_redirected_to :action => 'show', :id => '1'
2239 assert_redirected_to :action => 'show', :id => '1'
2177 issue.reload
2240 issue.reload
2178 assert_equal 'New custom value', issue.custom_value_for(2).value
2241 assert_equal 'New custom value', issue.custom_value_for(2).value
2179
2242
2180 mail = ActionMailer::Base.deliveries.last
2243 mail = ActionMailer::Base.deliveries.last
2181 assert_kind_of TMail::Mail, mail
2244 assert_kind_of TMail::Mail, mail
2182 assert mail.body.include?("Searchable field changed from 125 to New custom value")
2245 assert mail.body.include?("Searchable field changed from 125 to New custom value")
2183 end
2246 end
2184
2247
2185 def test_put_update_with_multi_custom_field_change
2248 def test_put_update_with_multi_custom_field_change
2186 field = CustomField.find(1)
2249 field = CustomField.find(1)
2187 field.update_attribute :multiple, true
2250 field.update_attribute :multiple, true
2188 issue = Issue.find(1)
2251 issue = Issue.find(1)
2189 issue.custom_field_values = {1 => ['MySQL', 'Oracle']}
2252 issue.custom_field_values = {1 => ['MySQL', 'Oracle']}
2190 issue.save!
2253 issue.save!
2191
2254
2192 @request.session[:user_id] = 2
2255 @request.session[:user_id] = 2
2193 assert_difference('Journal.count') do
2256 assert_difference('Journal.count') do
2194 assert_difference('JournalDetail.count', 3) do
2257 assert_difference('JournalDetail.count', 3) do
2195 put :update, :id => 1,
2258 put :update, :id => 1,
2196 :issue => {
2259 :issue => {
2197 :subject => 'Custom field change',
2260 :subject => 'Custom field change',
2198 :custom_field_values => { '1' => ['', 'Oracle', 'PostgreSQL'] }
2261 :custom_field_values => { '1' => ['', 'Oracle', 'PostgreSQL'] }
2199 }
2262 }
2200 end
2263 end
2201 end
2264 end
2202 assert_redirected_to :action => 'show', :id => '1'
2265 assert_redirected_to :action => 'show', :id => '1'
2203 assert_equal ['Oracle', 'PostgreSQL'], Issue.find(1).custom_field_value(1).sort
2266 assert_equal ['Oracle', 'PostgreSQL'], Issue.find(1).custom_field_value(1).sort
2204 end
2267 end
2205
2268
2206 def test_put_update_with_status_and_assignee_change
2269 def test_put_update_with_status_and_assignee_change
2207 issue = Issue.find(1)
2270 issue = Issue.find(1)
2208 assert_equal 1, issue.status_id
2271 assert_equal 1, issue.status_id
2209 @request.session[:user_id] = 2
2272 @request.session[:user_id] = 2
2210 assert_difference('TimeEntry.count', 0) do
2273 assert_difference('TimeEntry.count', 0) do
2211 put :update,
2274 put :update,
2212 :id => 1,
2275 :id => 1,
2213 :issue => { :status_id => 2, :assigned_to_id => 3 },
2276 :issue => { :status_id => 2, :assigned_to_id => 3 },
2214 :notes => 'Assigned to dlopper',
2277 :notes => 'Assigned to dlopper',
2215 :time_entry => { :hours => '', :comments => '', :activity_id => TimeEntryActivity.first }
2278 :time_entry => { :hours => '', :comments => '', :activity_id => TimeEntryActivity.first }
2216 end
2279 end
2217 assert_redirected_to :action => 'show', :id => '1'
2280 assert_redirected_to :action => 'show', :id => '1'
2218 issue.reload
2281 issue.reload
2219 assert_equal 2, issue.status_id
2282 assert_equal 2, issue.status_id
2220 j = Journal.find(:first, :order => 'id DESC')
2283 j = Journal.find(:first, :order => 'id DESC')
2221 assert_equal 'Assigned to dlopper', j.notes
2284 assert_equal 'Assigned to dlopper', j.notes
2222 assert_equal 2, j.details.size
2285 assert_equal 2, j.details.size
2223
2286
2224 mail = ActionMailer::Base.deliveries.last
2287 mail = ActionMailer::Base.deliveries.last
2225 assert mail.body.include?("Status changed from New to Assigned")
2288 assert mail.body.include?("Status changed from New to Assigned")
2226 # subject should contain the new status
2289 # subject should contain the new status
2227 assert mail.subject.include?("(#{ IssueStatus.find(2).name })")
2290 assert mail.subject.include?("(#{ IssueStatus.find(2).name })")
2228 end
2291 end
2229
2292
2230 def test_put_update_with_note_only
2293 def test_put_update_with_note_only
2231 notes = 'Note added by IssuesControllerTest#test_update_with_note_only'
2294 notes = 'Note added by IssuesControllerTest#test_update_with_note_only'
2232 # anonymous user
2295 # anonymous user
2233 put :update,
2296 put :update,
2234 :id => 1,
2297 :id => 1,
2235 :notes => notes
2298 :notes => notes
2236 assert_redirected_to :action => 'show', :id => '1'
2299 assert_redirected_to :action => 'show', :id => '1'
2237 j = Journal.find(:first, :order => 'id DESC')
2300 j = Journal.find(:first, :order => 'id DESC')
2238 assert_equal notes, j.notes
2301 assert_equal notes, j.notes
2239 assert_equal 0, j.details.size
2302 assert_equal 0, j.details.size
2240 assert_equal User.anonymous, j.user
2303 assert_equal User.anonymous, j.user
2241
2304
2242 mail = ActionMailer::Base.deliveries.last
2305 mail = ActionMailer::Base.deliveries.last
2243 assert mail.body.include?(notes)
2306 assert mail.body.include?(notes)
2244 end
2307 end
2245
2308
2246 def test_put_update_with_note_and_spent_time
2309 def test_put_update_with_note_and_spent_time
2247 @request.session[:user_id] = 2
2310 @request.session[:user_id] = 2
2248 spent_hours_before = Issue.find(1).spent_hours
2311 spent_hours_before = Issue.find(1).spent_hours
2249 assert_difference('TimeEntry.count') do
2312 assert_difference('TimeEntry.count') do
2250 put :update,
2313 put :update,
2251 :id => 1,
2314 :id => 1,
2252 :notes => '2.5 hours added',
2315 :notes => '2.5 hours added',
2253 :time_entry => { :hours => '2.5', :comments => 'test_put_update_with_note_and_spent_time', :activity_id => TimeEntryActivity.first.id }
2316 :time_entry => { :hours => '2.5', :comments => 'test_put_update_with_note_and_spent_time', :activity_id => TimeEntryActivity.first.id }
2254 end
2317 end
2255 assert_redirected_to :action => 'show', :id => '1'
2318 assert_redirected_to :action => 'show', :id => '1'
2256
2319
2257 issue = Issue.find(1)
2320 issue = Issue.find(1)
2258
2321
2259 j = Journal.find(:first, :order => 'id DESC')
2322 j = Journal.find(:first, :order => 'id DESC')
2260 assert_equal '2.5 hours added', j.notes
2323 assert_equal '2.5 hours added', j.notes
2261 assert_equal 0, j.details.size
2324 assert_equal 0, j.details.size
2262
2325
2263 t = issue.time_entries.find_by_comments('test_put_update_with_note_and_spent_time')
2326 t = issue.time_entries.find_by_comments('test_put_update_with_note_and_spent_time')
2264 assert_not_nil t
2327 assert_not_nil t
2265 assert_equal 2.5, t.hours
2328 assert_equal 2.5, t.hours
2266 assert_equal spent_hours_before + 2.5, issue.spent_hours
2329 assert_equal spent_hours_before + 2.5, issue.spent_hours
2267 end
2330 end
2268
2331
2269 def test_put_update_with_attachment_only
2332 def test_put_update_with_attachment_only
2270 set_tmp_attachments_directory
2333 set_tmp_attachments_directory
2271
2334
2272 # Delete all fixtured journals, a race condition can occur causing the wrong
2335 # Delete all fixtured journals, a race condition can occur causing the wrong
2273 # journal to get fetched in the next find.
2336 # journal to get fetched in the next find.
2274 Journal.delete_all
2337 Journal.delete_all
2275
2338
2276 # anonymous user
2339 # anonymous user
2277 assert_difference 'Attachment.count' do
2340 assert_difference 'Attachment.count' do
2278 put :update, :id => 1,
2341 put :update, :id => 1,
2279 :notes => '',
2342 :notes => '',
2280 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'test file'}}
2343 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'test file'}}
2281 end
2344 end
2282
2345
2283 assert_redirected_to :action => 'show', :id => '1'
2346 assert_redirected_to :action => 'show', :id => '1'
2284 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
2347 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
2285 assert j.notes.blank?
2348 assert j.notes.blank?
2286 assert_equal 1, j.details.size
2349 assert_equal 1, j.details.size
2287 assert_equal 'testfile.txt', j.details.first.value
2350 assert_equal 'testfile.txt', j.details.first.value
2288 assert_equal User.anonymous, j.user
2351 assert_equal User.anonymous, j.user
2289
2352
2290 attachment = Attachment.first(:order => 'id DESC')
2353 attachment = Attachment.first(:order => 'id DESC')
2291 assert_equal Issue.find(1), attachment.container
2354 assert_equal Issue.find(1), attachment.container
2292 assert_equal User.anonymous, attachment.author
2355 assert_equal User.anonymous, attachment.author
2293 assert_equal 'testfile.txt', attachment.filename
2356 assert_equal 'testfile.txt', attachment.filename
2294 assert_equal 'text/plain', attachment.content_type
2357 assert_equal 'text/plain', attachment.content_type
2295 assert_equal 'test file', attachment.description
2358 assert_equal 'test file', attachment.description
2296 assert_equal 59, attachment.filesize
2359 assert_equal 59, attachment.filesize
2297 assert File.exists?(attachment.diskfile)
2360 assert File.exists?(attachment.diskfile)
2298 assert_equal 59, File.size(attachment.diskfile)
2361 assert_equal 59, File.size(attachment.diskfile)
2299
2362
2300 mail = ActionMailer::Base.deliveries.last
2363 mail = ActionMailer::Base.deliveries.last
2301 assert mail.body.include?('testfile.txt')
2364 assert mail.body.include?('testfile.txt')
2302 end
2365 end
2303
2366
2367 def test_put_update_with_failure_should_save_attachments
2368 set_tmp_attachments_directory
2369 @request.session[:user_id] = 2
2370
2371 assert_no_difference 'Journal.count' do
2372 assert_difference 'Attachment.count' do
2373 put :update, :id => 1,
2374 :issue => { :subject => '' },
2375 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'test file'}}
2376 assert_response :success
2377 assert_template 'edit'
2378 end
2379 end
2380
2381 attachment = Attachment.first(:order => 'id DESC')
2382 assert_equal 'testfile.txt', attachment.filename
2383 assert File.exists?(attachment.diskfile)
2384 assert_nil attachment.container
2385
2386 assert_tag 'input', :attributes => {:name => 'attachments[p0][token]', :value => attachment.token}
2387 assert_tag 'span', :content => /testfile.txt/
2388 end
2389
2390 def test_put_update_with_failure_should_keep_saved_attachments
2391 set_tmp_attachments_directory
2392 attachment = Attachment.create!(:file => uploaded_test_file("testfile.txt", "text/plain"), :author_id => 2)
2393 @request.session[:user_id] = 2
2394
2395 assert_no_difference 'Journal.count' do
2396 assert_no_difference 'Attachment.count' do
2397 put :update, :id => 1,
2398 :issue => { :subject => '' },
2399 :attachments => {'p0' => {'token' => attachment.token}}
2400 assert_response :success
2401 assert_template 'edit'
2402 end
2403 end
2404
2405 assert_tag 'input', :attributes => {:name => 'attachments[p0][token]', :value => attachment.token}
2406 assert_tag 'span', :content => /testfile.txt/
2407 end
2408
2409 def test_put_update_should_attach_saved_attachments
2410 set_tmp_attachments_directory
2411 attachment = Attachment.create!(:file => uploaded_test_file("testfile.txt", "text/plain"), :author_id => 2)
2412 @request.session[:user_id] = 2
2413
2414 assert_difference 'Journal.count' do
2415 assert_difference 'JournalDetail.count' do
2416 assert_no_difference 'Attachment.count' do
2417 put :update, :id => 1,
2418 :notes => 'Attachment added',
2419 :attachments => {'p0' => {'token' => attachment.token}}
2420 assert_redirected_to '/issues/1'
2421 end
2422 end
2423 end
2424
2425 attachment.reload
2426 assert_equal Issue.find(1), attachment.container
2427
2428 journal = Journal.first(:order => 'id DESC')
2429 assert_equal 1, journal.details.size
2430 assert_equal 'testfile.txt', journal.details.first.value
2431 end
2432
2304 def test_put_update_with_attachment_that_fails_to_save
2433 def test_put_update_with_attachment_that_fails_to_save
2305 set_tmp_attachments_directory
2434 set_tmp_attachments_directory
2306
2435
2307 # Delete all fixtured journals, a race condition can occur causing the wrong
2436 # Delete all fixtured journals, a race condition can occur causing the wrong
2308 # journal to get fetched in the next find.
2437 # journal to get fetched in the next find.
2309 Journal.delete_all
2438 Journal.delete_all
2310
2439
2311 # Mock out the unsaved attachment
2440 # Mock out the unsaved attachment
2312 Attachment.any_instance.stubs(:create).returns(Attachment.new)
2441 Attachment.any_instance.stubs(:create).returns(Attachment.new)
2313
2442
2314 # anonymous user
2443 # anonymous user
2315 put :update,
2444 put :update,
2316 :id => 1,
2445 :id => 1,
2317 :notes => '',
2446 :notes => '',
2318 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
2447 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
2319 assert_redirected_to :action => 'show', :id => '1'
2448 assert_redirected_to :action => 'show', :id => '1'
2320 assert_equal '1 file(s) could not be saved.', flash[:warning]
2449 assert_equal '1 file(s) could not be saved.', flash[:warning]
2321 end
2450 end
2322
2451
2323 def test_put_update_with_no_change
2452 def test_put_update_with_no_change
2324 issue = Issue.find(1)
2453 issue = Issue.find(1)
2325 issue.journals.clear
2454 issue.journals.clear
2326 ActionMailer::Base.deliveries.clear
2455 ActionMailer::Base.deliveries.clear
2327
2456
2328 put :update,
2457 put :update,
2329 :id => 1,
2458 :id => 1,
2330 :notes => ''
2459 :notes => ''
2331 assert_redirected_to :action => 'show', :id => '1'
2460 assert_redirected_to :action => 'show', :id => '1'
2332
2461
2333 issue.reload
2462 issue.reload
2334 assert issue.journals.empty?
2463 assert issue.journals.empty?
2335 # No email should be sent
2464 # No email should be sent
2336 assert ActionMailer::Base.deliveries.empty?
2465 assert ActionMailer::Base.deliveries.empty?
2337 end
2466 end
2338
2467
2339 def test_put_update_should_send_a_notification
2468 def test_put_update_should_send_a_notification
2340 @request.session[:user_id] = 2
2469 @request.session[:user_id] = 2
2341 ActionMailer::Base.deliveries.clear
2470 ActionMailer::Base.deliveries.clear
2342 issue = Issue.find(1)
2471 issue = Issue.find(1)
2343 old_subject = issue.subject
2472 old_subject = issue.subject
2344 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
2473 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
2345
2474
2346 put :update, :id => 1, :issue => {:subject => new_subject,
2475 put :update, :id => 1, :issue => {:subject => new_subject,
2347 :priority_id => '6',
2476 :priority_id => '6',
2348 :category_id => '1' # no change
2477 :category_id => '1' # no change
2349 }
2478 }
2350 assert_equal 1, ActionMailer::Base.deliveries.size
2479 assert_equal 1, ActionMailer::Base.deliveries.size
2351 end
2480 end
2352
2481
2353 def test_put_update_with_invalid_spent_time_hours_only
2482 def test_put_update_with_invalid_spent_time_hours_only
2354 @request.session[:user_id] = 2
2483 @request.session[:user_id] = 2
2355 notes = 'Note added by IssuesControllerTest#test_post_edit_with_invalid_spent_time'
2484 notes = 'Note added by IssuesControllerTest#test_post_edit_with_invalid_spent_time'
2356
2485
2357 assert_no_difference('Journal.count') do
2486 assert_no_difference('Journal.count') do
2358 put :update,
2487 put :update,
2359 :id => 1,
2488 :id => 1,
2360 :notes => notes,
2489 :notes => notes,
2361 :time_entry => {"comments"=>"", "activity_id"=>"", "hours"=>"2z"}
2490 :time_entry => {"comments"=>"", "activity_id"=>"", "hours"=>"2z"}
2362 end
2491 end
2363 assert_response :success
2492 assert_response :success
2364 assert_template 'edit'
2493 assert_template 'edit'
2365
2494
2366 assert_error_tag :descendant => {:content => /Activity can't be blank/}
2495 assert_error_tag :descendant => {:content => /Activity can't be blank/}
2367 assert_tag :textarea, :attributes => { :name => 'notes' }, :content => notes
2496 assert_tag :textarea, :attributes => { :name => 'notes' }, :content => notes
2368 assert_tag :input, :attributes => { :name => 'time_entry[hours]', :value => "2z" }
2497 assert_tag :input, :attributes => { :name => 'time_entry[hours]', :value => "2z" }
2369 end
2498 end
2370
2499
2371 def test_put_update_with_invalid_spent_time_comments_only
2500 def test_put_update_with_invalid_spent_time_comments_only
2372 @request.session[:user_id] = 2
2501 @request.session[:user_id] = 2
2373 notes = 'Note added by IssuesControllerTest#test_post_edit_with_invalid_spent_time'
2502 notes = 'Note added by IssuesControllerTest#test_post_edit_with_invalid_spent_time'
2374
2503
2375 assert_no_difference('Journal.count') do
2504 assert_no_difference('Journal.count') do
2376 put :update,
2505 put :update,
2377 :id => 1,
2506 :id => 1,
2378 :notes => notes,
2507 :notes => notes,
2379 :time_entry => {"comments"=>"this is my comment", "activity_id"=>"", "hours"=>""}
2508 :time_entry => {"comments"=>"this is my comment", "activity_id"=>"", "hours"=>""}
2380 end
2509 end
2381 assert_response :success
2510 assert_response :success
2382 assert_template 'edit'
2511 assert_template 'edit'
2383
2512
2384 assert_error_tag :descendant => {:content => /Activity can't be blank/}
2513 assert_error_tag :descendant => {:content => /Activity can't be blank/}
2385 assert_error_tag :descendant => {:content => /Hours can't be blank/}
2514 assert_error_tag :descendant => {:content => /Hours can't be blank/}
2386 assert_tag :textarea, :attributes => { :name => 'notes' }, :content => notes
2515 assert_tag :textarea, :attributes => { :name => 'notes' }, :content => notes
2387 assert_tag :input, :attributes => { :name => 'time_entry[comments]', :value => "this is my comment" }
2516 assert_tag :input, :attributes => { :name => 'time_entry[comments]', :value => "this is my comment" }
2388 end
2517 end
2389
2518
2390 def test_put_update_should_allow_fixed_version_to_be_set_to_a_subproject
2519 def test_put_update_should_allow_fixed_version_to_be_set_to_a_subproject
2391 issue = Issue.find(2)
2520 issue = Issue.find(2)
2392 @request.session[:user_id] = 2
2521 @request.session[:user_id] = 2
2393
2522
2394 put :update,
2523 put :update,
2395 :id => issue.id,
2524 :id => issue.id,
2396 :issue => {
2525 :issue => {
2397 :fixed_version_id => 4
2526 :fixed_version_id => 4
2398 }
2527 }
2399
2528
2400 assert_response :redirect
2529 assert_response :redirect
2401 issue.reload
2530 issue.reload
2402 assert_equal 4, issue.fixed_version_id
2531 assert_equal 4, issue.fixed_version_id
2403 assert_not_equal issue.project_id, issue.fixed_version.project_id
2532 assert_not_equal issue.project_id, issue.fixed_version.project_id
2404 end
2533 end
2405
2534
2406 def test_put_update_should_redirect_back_using_the_back_url_parameter
2535 def test_put_update_should_redirect_back_using_the_back_url_parameter
2407 issue = Issue.find(2)
2536 issue = Issue.find(2)
2408 @request.session[:user_id] = 2
2537 @request.session[:user_id] = 2
2409
2538
2410 put :update,
2539 put :update,
2411 :id => issue.id,
2540 :id => issue.id,
2412 :issue => {
2541 :issue => {
2413 :fixed_version_id => 4
2542 :fixed_version_id => 4
2414 },
2543 },
2415 :back_url => '/issues'
2544 :back_url => '/issues'
2416
2545
2417 assert_response :redirect
2546 assert_response :redirect
2418 assert_redirected_to '/issues'
2547 assert_redirected_to '/issues'
2419 end
2548 end
2420
2549
2421 def test_put_update_should_not_redirect_back_using_the_back_url_parameter_off_the_host
2550 def test_put_update_should_not_redirect_back_using_the_back_url_parameter_off_the_host
2422 issue = Issue.find(2)
2551 issue = Issue.find(2)
2423 @request.session[:user_id] = 2
2552 @request.session[:user_id] = 2
2424
2553
2425 put :update,
2554 put :update,
2426 :id => issue.id,
2555 :id => issue.id,
2427 :issue => {
2556 :issue => {
2428 :fixed_version_id => 4
2557 :fixed_version_id => 4
2429 },
2558 },
2430 :back_url => 'http://google.com'
2559 :back_url => 'http://google.com'
2431
2560
2432 assert_response :redirect
2561 assert_response :redirect
2433 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue.id
2562 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue.id
2434 end
2563 end
2435
2564
2436 def test_get_bulk_edit
2565 def test_get_bulk_edit
2437 @request.session[:user_id] = 2
2566 @request.session[:user_id] = 2
2438 get :bulk_edit, :ids => [1, 2]
2567 get :bulk_edit, :ids => [1, 2]
2439 assert_response :success
2568 assert_response :success
2440 assert_template 'bulk_edit'
2569 assert_template 'bulk_edit'
2441
2570
2442 assert_tag :select, :attributes => {:name => 'issue[project_id]'}
2571 assert_tag :select, :attributes => {:name => 'issue[project_id]'}
2443 assert_tag :input, :attributes => {:name => 'issue[parent_issue_id]'}
2572 assert_tag :input, :attributes => {:name => 'issue[parent_issue_id]'}
2444
2573
2445 # Project specific custom field, date type
2574 # Project specific custom field, date type
2446 field = CustomField.find(9)
2575 field = CustomField.find(9)
2447 assert !field.is_for_all?
2576 assert !field.is_for_all?
2448 assert_equal 'date', field.field_format
2577 assert_equal 'date', field.field_format
2449 assert_tag :input, :attributes => {:name => 'issue[custom_field_values][9]'}
2578 assert_tag :input, :attributes => {:name => 'issue[custom_field_values][9]'}
2450
2579
2451 # System wide custom field
2580 # System wide custom field
2452 assert CustomField.find(1).is_for_all?
2581 assert CustomField.find(1).is_for_all?
2453 assert_tag :select, :attributes => {:name => 'issue[custom_field_values][1]'}
2582 assert_tag :select, :attributes => {:name => 'issue[custom_field_values][1]'}
2454
2583
2455 # Be sure we don't display inactive IssuePriorities
2584 # Be sure we don't display inactive IssuePriorities
2456 assert ! IssuePriority.find(15).active?
2585 assert ! IssuePriority.find(15).active?
2457 assert_no_tag :option, :attributes => {:value => '15'},
2586 assert_no_tag :option, :attributes => {:value => '15'},
2458 :parent => {:tag => 'select', :attributes => {:id => 'issue_priority_id'} }
2587 :parent => {:tag => 'select', :attributes => {:id => 'issue_priority_id'} }
2459 end
2588 end
2460
2589
2461 def test_get_bulk_edit_on_different_projects
2590 def test_get_bulk_edit_on_different_projects
2462 @request.session[:user_id] = 2
2591 @request.session[:user_id] = 2
2463 get :bulk_edit, :ids => [1, 2, 6]
2592 get :bulk_edit, :ids => [1, 2, 6]
2464 assert_response :success
2593 assert_response :success
2465 assert_template 'bulk_edit'
2594 assert_template 'bulk_edit'
2466
2595
2467 # Can not set issues from different projects as children of an issue
2596 # Can not set issues from different projects as children of an issue
2468 assert_no_tag :input, :attributes => {:name => 'issue[parent_issue_id]'}
2597 assert_no_tag :input, :attributes => {:name => 'issue[parent_issue_id]'}
2469
2598
2470 # Project specific custom field, date type
2599 # Project specific custom field, date type
2471 field = CustomField.find(9)
2600 field = CustomField.find(9)
2472 assert !field.is_for_all?
2601 assert !field.is_for_all?
2473 assert !field.project_ids.include?(Issue.find(6).project_id)
2602 assert !field.project_ids.include?(Issue.find(6).project_id)
2474 assert_no_tag :input, :attributes => {:name => 'issue[custom_field_values][9]'}
2603 assert_no_tag :input, :attributes => {:name => 'issue[custom_field_values][9]'}
2475 end
2604 end
2476
2605
2477 def test_get_bulk_edit_with_user_custom_field
2606 def test_get_bulk_edit_with_user_custom_field
2478 field = IssueCustomField.create!(:name => 'Tester', :field_format => 'user', :is_for_all => true)
2607 field = IssueCustomField.create!(:name => 'Tester', :field_format => 'user', :is_for_all => true)
2479
2608
2480 @request.session[:user_id] = 2
2609 @request.session[:user_id] = 2
2481 get :bulk_edit, :ids => [1, 2]
2610 get :bulk_edit, :ids => [1, 2]
2482 assert_response :success
2611 assert_response :success
2483 assert_template 'bulk_edit'
2612 assert_template 'bulk_edit'
2484
2613
2485 assert_tag :select,
2614 assert_tag :select,
2486 :attributes => {:name => "issue[custom_field_values][#{field.id}]"},
2615 :attributes => {:name => "issue[custom_field_values][#{field.id}]"},
2487 :children => {
2616 :children => {
2488 :only => {:tag => 'option'},
2617 :only => {:tag => 'option'},
2489 :count => Project.find(1).users.count + 1
2618 :count => Project.find(1).users.count + 1
2490 }
2619 }
2491 end
2620 end
2492
2621
2493 def test_get_bulk_edit_with_version_custom_field
2622 def test_get_bulk_edit_with_version_custom_field
2494 field = IssueCustomField.create!(:name => 'Affected version', :field_format => 'version', :is_for_all => true)
2623 field = IssueCustomField.create!(:name => 'Affected version', :field_format => 'version', :is_for_all => true)
2495
2624
2496 @request.session[:user_id] = 2
2625 @request.session[:user_id] = 2
2497 get :bulk_edit, :ids => [1, 2]
2626 get :bulk_edit, :ids => [1, 2]
2498 assert_response :success
2627 assert_response :success
2499 assert_template 'bulk_edit'
2628 assert_template 'bulk_edit'
2500
2629
2501 assert_tag :select,
2630 assert_tag :select,
2502 :attributes => {:name => "issue[custom_field_values][#{field.id}]"},
2631 :attributes => {:name => "issue[custom_field_values][#{field.id}]"},
2503 :children => {
2632 :children => {
2504 :only => {:tag => 'option'},
2633 :only => {:tag => 'option'},
2505 :count => Project.find(1).shared_versions.count + 1
2634 :count => Project.find(1).shared_versions.count + 1
2506 }
2635 }
2507 end
2636 end
2508
2637
2509 def test_get_bulk_edit_with_multi_custom_field
2638 def test_get_bulk_edit_with_multi_custom_field
2510 field = CustomField.find(1)
2639 field = CustomField.find(1)
2511 field.update_attribute :multiple, true
2640 field.update_attribute :multiple, true
2512
2641
2513 @request.session[:user_id] = 2
2642 @request.session[:user_id] = 2
2514 get :bulk_edit, :ids => [1, 2]
2643 get :bulk_edit, :ids => [1, 2]
2515 assert_response :success
2644 assert_response :success
2516 assert_template 'bulk_edit'
2645 assert_template 'bulk_edit'
2517
2646
2518 assert_tag :select,
2647 assert_tag :select,
2519 :attributes => {:name => "issue[custom_field_values][1][]"},
2648 :attributes => {:name => "issue[custom_field_values][1][]"},
2520 :children => {
2649 :children => {
2521 :only => {:tag => 'option'},
2650 :only => {:tag => 'option'},
2522 :count => 3
2651 :count => 3
2523 }
2652 }
2524 end
2653 end
2525
2654
2526 def test_bulk_edit_should_only_propose_statuses_allowed_for_all_issues
2655 def test_bulk_edit_should_only_propose_statuses_allowed_for_all_issues
2527 Workflow.delete_all
2656 Workflow.delete_all
2528 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 1)
2657 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 1)
2529 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 3)
2658 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 3)
2530 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 4)
2659 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 4)
2531 Workflow.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :new_status_id => 1)
2660 Workflow.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :new_status_id => 1)
2532 Workflow.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :new_status_id => 3)
2661 Workflow.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :new_status_id => 3)
2533 Workflow.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :new_status_id => 5)
2662 Workflow.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :new_status_id => 5)
2534 @request.session[:user_id] = 2
2663 @request.session[:user_id] = 2
2535 get :bulk_edit, :ids => [1, 2]
2664 get :bulk_edit, :ids => [1, 2]
2536
2665
2537 assert_response :success
2666 assert_response :success
2538 statuses = assigns(:available_statuses)
2667 statuses = assigns(:available_statuses)
2539 assert_not_nil statuses
2668 assert_not_nil statuses
2540 assert_equal [1, 3], statuses.map(&:id).sort
2669 assert_equal [1, 3], statuses.map(&:id).sort
2541
2670
2542 assert_tag 'select', :attributes => {:name => 'issue[status_id]'},
2671 assert_tag 'select', :attributes => {:name => 'issue[status_id]'},
2543 :children => {:count => 3} # 2 statuses + "no change" option
2672 :children => {:count => 3} # 2 statuses + "no change" option
2544 end
2673 end
2545
2674
2546 def test_bulk_update
2675 def test_bulk_update
2547 @request.session[:user_id] = 2
2676 @request.session[:user_id] = 2
2548 # update issues priority
2677 # update issues priority
2549 post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing',
2678 post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing',
2550 :issue => {:priority_id => 7,
2679 :issue => {:priority_id => 7,
2551 :assigned_to_id => '',
2680 :assigned_to_id => '',
2552 :custom_field_values => {'2' => ''}}
2681 :custom_field_values => {'2' => ''}}
2553
2682
2554 assert_response 302
2683 assert_response 302
2555 # check that the issues were updated
2684 # check that the issues were updated
2556 assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id}
2685 assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id}
2557
2686
2558 issue = Issue.find(1)
2687 issue = Issue.find(1)
2559 journal = issue.journals.find(:first, :order => 'created_on DESC')
2688 journal = issue.journals.find(:first, :order => 'created_on DESC')
2560 assert_equal '125', issue.custom_value_for(2).value
2689 assert_equal '125', issue.custom_value_for(2).value
2561 assert_equal 'Bulk editing', journal.notes
2690 assert_equal 'Bulk editing', journal.notes
2562 assert_equal 1, journal.details.size
2691 assert_equal 1, journal.details.size
2563 end
2692 end
2564
2693
2565 def test_bulk_update_with_group_assignee
2694 def test_bulk_update_with_group_assignee
2566 group = Group.find(11)
2695 group = Group.find(11)
2567 project = Project.find(1)
2696 project = Project.find(1)
2568 project.members << Member.new(:principal => group, :roles => [Role.first])
2697 project.members << Member.new(:principal => group, :roles => [Role.first])
2569
2698
2570 @request.session[:user_id] = 2
2699 @request.session[:user_id] = 2
2571 # update issues assignee
2700 # update issues assignee
2572 post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing',
2701 post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing',
2573 :issue => {:priority_id => '',
2702 :issue => {:priority_id => '',
2574 :assigned_to_id => group.id,
2703 :assigned_to_id => group.id,
2575 :custom_field_values => {'2' => ''}}
2704 :custom_field_values => {'2' => ''}}
2576
2705
2577 assert_response 302
2706 assert_response 302
2578 assert_equal [group, group], Issue.find_all_by_id([1, 2]).collect {|i| i.assigned_to}
2707 assert_equal [group, group], Issue.find_all_by_id([1, 2]).collect {|i| i.assigned_to}
2579 end
2708 end
2580
2709
2581 def test_bulk_update_on_different_projects
2710 def test_bulk_update_on_different_projects
2582 @request.session[:user_id] = 2
2711 @request.session[:user_id] = 2
2583 # update issues priority
2712 # update issues priority
2584 post :bulk_update, :ids => [1, 2, 6], :notes => 'Bulk editing',
2713 post :bulk_update, :ids => [1, 2, 6], :notes => 'Bulk editing',
2585 :issue => {:priority_id => 7,
2714 :issue => {:priority_id => 7,
2586 :assigned_to_id => '',
2715 :assigned_to_id => '',
2587 :custom_field_values => {'2' => ''}}
2716 :custom_field_values => {'2' => ''}}
2588
2717
2589 assert_response 302
2718 assert_response 302
2590 # check that the issues were updated
2719 # check that the issues were updated
2591 assert_equal [7, 7, 7], Issue.find([1,2,6]).map(&:priority_id)
2720 assert_equal [7, 7, 7], Issue.find([1,2,6]).map(&:priority_id)
2592
2721
2593 issue = Issue.find(1)
2722 issue = Issue.find(1)
2594 journal = issue.journals.find(:first, :order => 'created_on DESC')
2723 journal = issue.journals.find(:first, :order => 'created_on DESC')
2595 assert_equal '125', issue.custom_value_for(2).value
2724 assert_equal '125', issue.custom_value_for(2).value
2596 assert_equal 'Bulk editing', journal.notes
2725 assert_equal 'Bulk editing', journal.notes
2597 assert_equal 1, journal.details.size
2726 assert_equal 1, journal.details.size
2598 end
2727 end
2599
2728
2600 def test_bulk_update_on_different_projects_without_rights
2729 def test_bulk_update_on_different_projects_without_rights
2601 @request.session[:user_id] = 3
2730 @request.session[:user_id] = 3
2602 user = User.find(3)
2731 user = User.find(3)
2603 action = { :controller => "issues", :action => "bulk_update" }
2732 action = { :controller => "issues", :action => "bulk_update" }
2604 assert user.allowed_to?(action, Issue.find(1).project)
2733 assert user.allowed_to?(action, Issue.find(1).project)
2605 assert ! user.allowed_to?(action, Issue.find(6).project)
2734 assert ! user.allowed_to?(action, Issue.find(6).project)
2606 post :bulk_update, :ids => [1, 6], :notes => 'Bulk should fail',
2735 post :bulk_update, :ids => [1, 6], :notes => 'Bulk should fail',
2607 :issue => {:priority_id => 7,
2736 :issue => {:priority_id => 7,
2608 :assigned_to_id => '',
2737 :assigned_to_id => '',
2609 :custom_field_values => {'2' => ''}}
2738 :custom_field_values => {'2' => ''}}
2610 assert_response 403
2739 assert_response 403
2611 assert_not_equal "Bulk should fail", Journal.last.notes
2740 assert_not_equal "Bulk should fail", Journal.last.notes
2612 end
2741 end
2613
2742
2614 def test_bullk_update_should_send_a_notification
2743 def test_bullk_update_should_send_a_notification
2615 @request.session[:user_id] = 2
2744 @request.session[:user_id] = 2
2616 ActionMailer::Base.deliveries.clear
2745 ActionMailer::Base.deliveries.clear
2617 post(:bulk_update,
2746 post(:bulk_update,
2618 {
2747 {
2619 :ids => [1, 2],
2748 :ids => [1, 2],
2620 :notes => 'Bulk editing',
2749 :notes => 'Bulk editing',
2621 :issue => {
2750 :issue => {
2622 :priority_id => 7,
2751 :priority_id => 7,
2623 :assigned_to_id => '',
2752 :assigned_to_id => '',
2624 :custom_field_values => {'2' => ''}
2753 :custom_field_values => {'2' => ''}
2625 }
2754 }
2626 })
2755 })
2627
2756
2628 assert_response 302
2757 assert_response 302
2629 assert_equal 2, ActionMailer::Base.deliveries.size
2758 assert_equal 2, ActionMailer::Base.deliveries.size
2630 end
2759 end
2631
2760
2632 def test_bulk_update_project
2761 def test_bulk_update_project
2633 @request.session[:user_id] = 2
2762 @request.session[:user_id] = 2
2634 post :bulk_update, :ids => [1, 2], :issue => {:project_id => '2'}
2763 post :bulk_update, :ids => [1, 2], :issue => {:project_id => '2'}
2635 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook'
2764 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook'
2636 # Issues moved to project 2
2765 # Issues moved to project 2
2637 assert_equal 2, Issue.find(1).project_id
2766 assert_equal 2, Issue.find(1).project_id
2638 assert_equal 2, Issue.find(2).project_id
2767 assert_equal 2, Issue.find(2).project_id
2639 # No tracker change
2768 # No tracker change
2640 assert_equal 1, Issue.find(1).tracker_id
2769 assert_equal 1, Issue.find(1).tracker_id
2641 assert_equal 2, Issue.find(2).tracker_id
2770 assert_equal 2, Issue.find(2).tracker_id
2642 end
2771 end
2643
2772
2644 def test_bulk_update_project_on_single_issue_should_follow_when_needed
2773 def test_bulk_update_project_on_single_issue_should_follow_when_needed
2645 @request.session[:user_id] = 2
2774 @request.session[:user_id] = 2
2646 post :bulk_update, :id => 1, :issue => {:project_id => '2'}, :follow => '1'
2775 post :bulk_update, :id => 1, :issue => {:project_id => '2'}, :follow => '1'
2647 assert_redirected_to '/issues/1'
2776 assert_redirected_to '/issues/1'
2648 end
2777 end
2649
2778
2650 def test_bulk_update_project_on_multiple_issues_should_follow_when_needed
2779 def test_bulk_update_project_on_multiple_issues_should_follow_when_needed
2651 @request.session[:user_id] = 2
2780 @request.session[:user_id] = 2
2652 post :bulk_update, :id => [1, 2], :issue => {:project_id => '2'}, :follow => '1'
2781 post :bulk_update, :id => [1, 2], :issue => {:project_id => '2'}, :follow => '1'
2653 assert_redirected_to '/projects/onlinestore/issues'
2782 assert_redirected_to '/projects/onlinestore/issues'
2654 end
2783 end
2655
2784
2656 def test_bulk_update_tracker
2785 def test_bulk_update_tracker
2657 @request.session[:user_id] = 2
2786 @request.session[:user_id] = 2
2658 post :bulk_update, :ids => [1, 2], :issue => {:tracker_id => '2'}
2787 post :bulk_update, :ids => [1, 2], :issue => {:tracker_id => '2'}
2659 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook'
2788 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook'
2660 assert_equal 2, Issue.find(1).tracker_id
2789 assert_equal 2, Issue.find(1).tracker_id
2661 assert_equal 2, Issue.find(2).tracker_id
2790 assert_equal 2, Issue.find(2).tracker_id
2662 end
2791 end
2663
2792
2664 def test_bulk_update_status
2793 def test_bulk_update_status
2665 @request.session[:user_id] = 2
2794 @request.session[:user_id] = 2
2666 # update issues priority
2795 # update issues priority
2667 post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing status',
2796 post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing status',
2668 :issue => {:priority_id => '',
2797 :issue => {:priority_id => '',
2669 :assigned_to_id => '',
2798 :assigned_to_id => '',
2670 :status_id => '5'}
2799 :status_id => '5'}
2671
2800
2672 assert_response 302
2801 assert_response 302
2673 issue = Issue.find(1)
2802 issue = Issue.find(1)
2674 assert issue.closed?
2803 assert issue.closed?
2675 end
2804 end
2676
2805
2677 def test_bulk_update_priority
2806 def test_bulk_update_priority
2678 @request.session[:user_id] = 2
2807 @request.session[:user_id] = 2
2679 post :bulk_update, :ids => [1, 2], :issue => {:priority_id => 6}
2808 post :bulk_update, :ids => [1, 2], :issue => {:priority_id => 6}
2680
2809
2681 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook'
2810 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook'
2682 assert_equal 6, Issue.find(1).priority_id
2811 assert_equal 6, Issue.find(1).priority_id
2683 assert_equal 6, Issue.find(2).priority_id
2812 assert_equal 6, Issue.find(2).priority_id
2684 end
2813 end
2685
2814
2686 def test_bulk_update_with_notes
2815 def test_bulk_update_with_notes
2687 @request.session[:user_id] = 2
2816 @request.session[:user_id] = 2
2688 post :bulk_update, :ids => [1, 2], :notes => 'Moving two issues'
2817 post :bulk_update, :ids => [1, 2], :notes => 'Moving two issues'
2689
2818
2690 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook'
2819 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook'
2691 assert_equal 'Moving two issues', Issue.find(1).journals.sort_by(&:id).last.notes
2820 assert_equal 'Moving two issues', Issue.find(1).journals.sort_by(&:id).last.notes
2692 assert_equal 'Moving two issues', Issue.find(2).journals.sort_by(&:id).last.notes
2821 assert_equal 'Moving two issues', Issue.find(2).journals.sort_by(&:id).last.notes
2693 end
2822 end
2694
2823
2695 def test_bulk_update_parent_id
2824 def test_bulk_update_parent_id
2696 @request.session[:user_id] = 2
2825 @request.session[:user_id] = 2
2697 post :bulk_update, :ids => [1, 3],
2826 post :bulk_update, :ids => [1, 3],
2698 :notes => 'Bulk editing parent',
2827 :notes => 'Bulk editing parent',
2699 :issue => {:priority_id => '', :assigned_to_id => '', :status_id => '', :parent_issue_id => '2'}
2828 :issue => {:priority_id => '', :assigned_to_id => '', :status_id => '', :parent_issue_id => '2'}
2700
2829
2701 assert_response 302
2830 assert_response 302
2702 parent = Issue.find(2)
2831 parent = Issue.find(2)
2703 assert_equal parent.id, Issue.find(1).parent_id
2832 assert_equal parent.id, Issue.find(1).parent_id
2704 assert_equal parent.id, Issue.find(3).parent_id
2833 assert_equal parent.id, Issue.find(3).parent_id
2705 assert_equal [1, 3], parent.children.collect(&:id).sort
2834 assert_equal [1, 3], parent.children.collect(&:id).sort
2706 end
2835 end
2707
2836
2708 def test_bulk_update_custom_field
2837 def test_bulk_update_custom_field
2709 @request.session[:user_id] = 2
2838 @request.session[:user_id] = 2
2710 # update issues priority
2839 # update issues priority
2711 post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing custom field',
2840 post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing custom field',
2712 :issue => {:priority_id => '',
2841 :issue => {:priority_id => '',
2713 :assigned_to_id => '',
2842 :assigned_to_id => '',
2714 :custom_field_values => {'2' => '777'}}
2843 :custom_field_values => {'2' => '777'}}
2715
2844
2716 assert_response 302
2845 assert_response 302
2717
2846
2718 issue = Issue.find(1)
2847 issue = Issue.find(1)
2719 journal = issue.journals.find(:first, :order => 'created_on DESC')
2848 journal = issue.journals.find(:first, :order => 'created_on DESC')
2720 assert_equal '777', issue.custom_value_for(2).value
2849 assert_equal '777', issue.custom_value_for(2).value
2721 assert_equal 1, journal.details.size
2850 assert_equal 1, journal.details.size
2722 assert_equal '125', journal.details.first.old_value
2851 assert_equal '125', journal.details.first.old_value
2723 assert_equal '777', journal.details.first.value
2852 assert_equal '777', journal.details.first.value
2724 end
2853 end
2725
2854
2726 def test_bulk_update_multi_custom_field
2855 def test_bulk_update_multi_custom_field
2727 field = CustomField.find(1)
2856 field = CustomField.find(1)
2728 field.update_attribute :multiple, true
2857 field.update_attribute :multiple, true
2729
2858
2730 @request.session[:user_id] = 2
2859 @request.session[:user_id] = 2
2731 post :bulk_update, :ids => [1, 2, 3], :notes => 'Bulk editing multi custom field',
2860 post :bulk_update, :ids => [1, 2, 3], :notes => 'Bulk editing multi custom field',
2732 :issue => {:priority_id => '',
2861 :issue => {:priority_id => '',
2733 :assigned_to_id => '',
2862 :assigned_to_id => '',
2734 :custom_field_values => {'1' => ['MySQL', 'Oracle']}}
2863 :custom_field_values => {'1' => ['MySQL', 'Oracle']}}
2735
2864
2736 assert_response 302
2865 assert_response 302
2737
2866
2738 assert_equal ['MySQL', 'Oracle'], Issue.find(1).custom_field_value(1).sort
2867 assert_equal ['MySQL', 'Oracle'], Issue.find(1).custom_field_value(1).sort
2739 assert_equal ['MySQL', 'Oracle'], Issue.find(3).custom_field_value(1).sort
2868 assert_equal ['MySQL', 'Oracle'], Issue.find(3).custom_field_value(1).sort
2740 # the custom field is not associated with the issue tracker
2869 # the custom field is not associated with the issue tracker
2741 assert_nil Issue.find(2).custom_field_value(1)
2870 assert_nil Issue.find(2).custom_field_value(1)
2742 end
2871 end
2743
2872
2744 def test_bulk_update_unassign
2873 def test_bulk_update_unassign
2745 assert_not_nil Issue.find(2).assigned_to
2874 assert_not_nil Issue.find(2).assigned_to
2746 @request.session[:user_id] = 2
2875 @request.session[:user_id] = 2
2747 # unassign issues
2876 # unassign issues
2748 post :bulk_update, :ids => [1, 2], :notes => 'Bulk unassigning', :issue => {:assigned_to_id => 'none'}
2877 post :bulk_update, :ids => [1, 2], :notes => 'Bulk unassigning', :issue => {:assigned_to_id => 'none'}
2749 assert_response 302
2878 assert_response 302
2750 # check that the issues were updated
2879 # check that the issues were updated
2751 assert_nil Issue.find(2).assigned_to
2880 assert_nil Issue.find(2).assigned_to
2752 end
2881 end
2753
2882
2754 def test_post_bulk_update_should_allow_fixed_version_to_be_set_to_a_subproject
2883 def test_post_bulk_update_should_allow_fixed_version_to_be_set_to_a_subproject
2755 @request.session[:user_id] = 2
2884 @request.session[:user_id] = 2
2756
2885
2757 post :bulk_update, :ids => [1,2], :issue => {:fixed_version_id => 4}
2886 post :bulk_update, :ids => [1,2], :issue => {:fixed_version_id => 4}
2758
2887
2759 assert_response :redirect
2888 assert_response :redirect
2760 issues = Issue.find([1,2])
2889 issues = Issue.find([1,2])
2761 issues.each do |issue|
2890 issues.each do |issue|
2762 assert_equal 4, issue.fixed_version_id
2891 assert_equal 4, issue.fixed_version_id
2763 assert_not_equal issue.project_id, issue.fixed_version.project_id
2892 assert_not_equal issue.project_id, issue.fixed_version.project_id
2764 end
2893 end
2765 end
2894 end
2766
2895
2767 def test_post_bulk_update_should_redirect_back_using_the_back_url_parameter
2896 def test_post_bulk_update_should_redirect_back_using_the_back_url_parameter
2768 @request.session[:user_id] = 2
2897 @request.session[:user_id] = 2
2769 post :bulk_update, :ids => [1,2], :back_url => '/issues'
2898 post :bulk_update, :ids => [1,2], :back_url => '/issues'
2770
2899
2771 assert_response :redirect
2900 assert_response :redirect
2772 assert_redirected_to '/issues'
2901 assert_redirected_to '/issues'
2773 end
2902 end
2774
2903
2775 def test_post_bulk_update_should_not_redirect_back_using_the_back_url_parameter_off_the_host
2904 def test_post_bulk_update_should_not_redirect_back_using_the_back_url_parameter_off_the_host
2776 @request.session[:user_id] = 2
2905 @request.session[:user_id] = 2
2777 post :bulk_update, :ids => [1,2], :back_url => 'http://google.com'
2906 post :bulk_update, :ids => [1,2], :back_url => 'http://google.com'
2778
2907
2779 assert_response :redirect
2908 assert_response :redirect
2780 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => Project.find(1).identifier
2909 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => Project.find(1).identifier
2781 end
2910 end
2782
2911
2783 def test_bulk_copy_to_another_project
2912 def test_bulk_copy_to_another_project
2784 @request.session[:user_id] = 2
2913 @request.session[:user_id] = 2
2785 assert_difference 'Issue.count', 2 do
2914 assert_difference 'Issue.count', 2 do
2786 assert_no_difference 'Project.find(1).issues.count' do
2915 assert_no_difference 'Project.find(1).issues.count' do
2787 post :bulk_update, :ids => [1, 2], :issue => {:project_id => '2'}, :copy => '1'
2916 post :bulk_update, :ids => [1, 2], :issue => {:project_id => '2'}, :copy => '1'
2788 end
2917 end
2789 end
2918 end
2790 assert_redirected_to '/projects/ecookbook/issues'
2919 assert_redirected_to '/projects/ecookbook/issues'
2791 end
2920 end
2792
2921
2793 def test_bulk_copy_should_allow_not_changing_the_issue_attributes
2922 def test_bulk_copy_should_allow_not_changing_the_issue_attributes
2794 @request.session[:user_id] = 2
2923 @request.session[:user_id] = 2
2795 issue_before_move = Issue.find(1)
2924 issue_before_move = Issue.find(1)
2796 assert_difference 'Issue.count', 1 do
2925 assert_difference 'Issue.count', 1 do
2797 assert_no_difference 'Project.find(1).issues.count' do
2926 assert_no_difference 'Project.find(1).issues.count' do
2798 post :bulk_update, :ids => [1], :copy => '1',
2927 post :bulk_update, :ids => [1], :copy => '1',
2799 :issue => {
2928 :issue => {
2800 :project_id => '2', :tracker_id => '', :assigned_to_id => '',
2929 :project_id => '2', :tracker_id => '', :assigned_to_id => '',
2801 :status_id => '', :start_date => '', :due_date => ''
2930 :status_id => '', :start_date => '', :due_date => ''
2802 }
2931 }
2803 end
2932 end
2804 end
2933 end
2805 issue_after_move = Issue.first(:order => 'id desc', :conditions => {:project_id => 2})
2934 issue_after_move = Issue.first(:order => 'id desc', :conditions => {:project_id => 2})
2806 assert_equal issue_before_move.tracker_id, issue_after_move.tracker_id
2935 assert_equal issue_before_move.tracker_id, issue_after_move.tracker_id
2807 assert_equal issue_before_move.status_id, issue_after_move.status_id
2936 assert_equal issue_before_move.status_id, issue_after_move.status_id
2808 assert_equal issue_before_move.assigned_to_id, issue_after_move.assigned_to_id
2937 assert_equal issue_before_move.assigned_to_id, issue_after_move.assigned_to_id
2809 end
2938 end
2810
2939
2811 def test_bulk_copy_should_allow_changing_the_issue_attributes
2940 def test_bulk_copy_should_allow_changing_the_issue_attributes
2812 # Fixes random test failure with Mysql
2941 # Fixes random test failure with Mysql
2813 # where Issue.all(:limit => 2, :order => 'id desc', :conditions => {:project_id => 2})
2942 # where Issue.all(:limit => 2, :order => 'id desc', :conditions => {:project_id => 2})
2814 # doesn't return the expected results
2943 # doesn't return the expected results
2815 Issue.delete_all("project_id=2")
2944 Issue.delete_all("project_id=2")
2816
2945
2817 @request.session[:user_id] = 2
2946 @request.session[:user_id] = 2
2818 assert_difference 'Issue.count', 2 do
2947 assert_difference 'Issue.count', 2 do
2819 assert_no_difference 'Project.find(1).issues.count' do
2948 assert_no_difference 'Project.find(1).issues.count' do
2820 post :bulk_update, :ids => [1, 2], :copy => '1',
2949 post :bulk_update, :ids => [1, 2], :copy => '1',
2821 :issue => {
2950 :issue => {
2822 :project_id => '2', :tracker_id => '', :assigned_to_id => '4',
2951 :project_id => '2', :tracker_id => '', :assigned_to_id => '4',
2823 :status_id => '3', :start_date => '2009-12-01', :due_date => '2009-12-31'
2952 :status_id => '3', :start_date => '2009-12-01', :due_date => '2009-12-31'
2824 }
2953 }
2825 end
2954 end
2826 end
2955 end
2827
2956
2828 copied_issues = Issue.all(:limit => 2, :order => 'id desc', :conditions => {:project_id => 2})
2957 copied_issues = Issue.all(:limit => 2, :order => 'id desc', :conditions => {:project_id => 2})
2829 assert_equal 2, copied_issues.size
2958 assert_equal 2, copied_issues.size
2830 copied_issues.each do |issue|
2959 copied_issues.each do |issue|
2831 assert_equal 2, issue.project_id, "Project is incorrect"
2960 assert_equal 2, issue.project_id, "Project is incorrect"
2832 assert_equal 4, issue.assigned_to_id, "Assigned to is incorrect"
2961 assert_equal 4, issue.assigned_to_id, "Assigned to is incorrect"
2833 assert_equal 3, issue.status_id, "Status is incorrect"
2962 assert_equal 3, issue.status_id, "Status is incorrect"
2834 assert_equal '2009-12-01', issue.start_date.to_s, "Start date is incorrect"
2963 assert_equal '2009-12-01', issue.start_date.to_s, "Start date is incorrect"
2835 assert_equal '2009-12-31', issue.due_date.to_s, "Due date is incorrect"
2964 assert_equal '2009-12-31', issue.due_date.to_s, "Due date is incorrect"
2836 end
2965 end
2837 end
2966 end
2838
2967
2839 def test_bulk_copy_should_allow_adding_a_note
2968 def test_bulk_copy_should_allow_adding_a_note
2840 @request.session[:user_id] = 2
2969 @request.session[:user_id] = 2
2841 assert_difference 'Issue.count', 1 do
2970 assert_difference 'Issue.count', 1 do
2842 post :bulk_update, :ids => [1], :copy => '1',
2971 post :bulk_update, :ids => [1], :copy => '1',
2843 :notes => 'Copying one issue',
2972 :notes => 'Copying one issue',
2844 :issue => {
2973 :issue => {
2845 :project_id => '', :tracker_id => '', :assigned_to_id => '4',
2974 :project_id => '', :tracker_id => '', :assigned_to_id => '4',
2846 :status_id => '3', :start_date => '2009-12-01', :due_date => '2009-12-31'
2975 :status_id => '3', :start_date => '2009-12-01', :due_date => '2009-12-31'
2847 }
2976 }
2848 end
2977 end
2849
2978
2850 issue = Issue.first(:order => 'id DESC')
2979 issue = Issue.first(:order => 'id DESC')
2851 assert_equal 1, issue.journals.size
2980 assert_equal 1, issue.journals.size
2852 journal = issue.journals.first
2981 journal = issue.journals.first
2853 assert_equal 0, journal.details.size
2982 assert_equal 0, journal.details.size
2854 assert_equal 'Copying one issue', journal.notes
2983 assert_equal 'Copying one issue', journal.notes
2855 end
2984 end
2856
2985
2857 def test_bulk_copy_to_another_project_should_follow_when_needed
2986 def test_bulk_copy_to_another_project_should_follow_when_needed
2858 @request.session[:user_id] = 2
2987 @request.session[:user_id] = 2
2859 post :bulk_update, :ids => [1], :copy => '1', :issue => {:project_id => 2}, :follow => '1'
2988 post :bulk_update, :ids => [1], :copy => '1', :issue => {:project_id => 2}, :follow => '1'
2860 issue = Issue.first(:order => 'id DESC')
2989 issue = Issue.first(:order => 'id DESC')
2861 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
2990 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
2862 end
2991 end
2863
2992
2864 def test_destroy_issue_with_no_time_entries
2993 def test_destroy_issue_with_no_time_entries
2865 assert_nil TimeEntry.find_by_issue_id(2)
2994 assert_nil TimeEntry.find_by_issue_id(2)
2866 @request.session[:user_id] = 2
2995 @request.session[:user_id] = 2
2867
2996
2868 assert_difference 'Issue.count', -1 do
2997 assert_difference 'Issue.count', -1 do
2869 delete :destroy, :id => 2
2998 delete :destroy, :id => 2
2870 end
2999 end
2871 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
3000 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
2872 assert_nil Issue.find_by_id(2)
3001 assert_nil Issue.find_by_id(2)
2873 end
3002 end
2874
3003
2875 def test_destroy_issues_with_time_entries
3004 def test_destroy_issues_with_time_entries
2876 @request.session[:user_id] = 2
3005 @request.session[:user_id] = 2
2877
3006
2878 assert_no_difference 'Issue.count' do
3007 assert_no_difference 'Issue.count' do
2879 delete :destroy, :ids => [1, 3]
3008 delete :destroy, :ids => [1, 3]
2880 end
3009 end
2881 assert_response :success
3010 assert_response :success
2882 assert_template 'destroy'
3011 assert_template 'destroy'
2883 assert_not_nil assigns(:hours)
3012 assert_not_nil assigns(:hours)
2884 assert Issue.find_by_id(1) && Issue.find_by_id(3)
3013 assert Issue.find_by_id(1) && Issue.find_by_id(3)
2885 assert_tag 'form',
3014 assert_tag 'form',
2886 :descendant => {:tag => 'input', :attributes => {:name => '_method', :value => 'delete'}}
3015 :descendant => {:tag => 'input', :attributes => {:name => '_method', :value => 'delete'}}
2887 end
3016 end
2888
3017
2889 def test_destroy_issues_and_destroy_time_entries
3018 def test_destroy_issues_and_destroy_time_entries
2890 @request.session[:user_id] = 2
3019 @request.session[:user_id] = 2
2891
3020
2892 assert_difference 'Issue.count', -2 do
3021 assert_difference 'Issue.count', -2 do
2893 assert_difference 'TimeEntry.count', -3 do
3022 assert_difference 'TimeEntry.count', -3 do
2894 delete :destroy, :ids => [1, 3], :todo => 'destroy'
3023 delete :destroy, :ids => [1, 3], :todo => 'destroy'
2895 end
3024 end
2896 end
3025 end
2897 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
3026 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
2898 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
3027 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
2899 assert_nil TimeEntry.find_by_id([1, 2])
3028 assert_nil TimeEntry.find_by_id([1, 2])
2900 end
3029 end
2901
3030
2902 def test_destroy_issues_and_assign_time_entries_to_project
3031 def test_destroy_issues_and_assign_time_entries_to_project
2903 @request.session[:user_id] = 2
3032 @request.session[:user_id] = 2
2904
3033
2905 assert_difference 'Issue.count', -2 do
3034 assert_difference 'Issue.count', -2 do
2906 assert_no_difference 'TimeEntry.count' do
3035 assert_no_difference 'TimeEntry.count' do
2907 delete :destroy, :ids => [1, 3], :todo => 'nullify'
3036 delete :destroy, :ids => [1, 3], :todo => 'nullify'
2908 end
3037 end
2909 end
3038 end
2910 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
3039 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
2911 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
3040 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
2912 assert_nil TimeEntry.find(1).issue_id
3041 assert_nil TimeEntry.find(1).issue_id
2913 assert_nil TimeEntry.find(2).issue_id
3042 assert_nil TimeEntry.find(2).issue_id
2914 end
3043 end
2915
3044
2916 def test_destroy_issues_and_reassign_time_entries_to_another_issue
3045 def test_destroy_issues_and_reassign_time_entries_to_another_issue
2917 @request.session[:user_id] = 2
3046 @request.session[:user_id] = 2
2918
3047
2919 assert_difference 'Issue.count', -2 do
3048 assert_difference 'Issue.count', -2 do
2920 assert_no_difference 'TimeEntry.count' do
3049 assert_no_difference 'TimeEntry.count' do
2921 delete :destroy, :ids => [1, 3], :todo => 'reassign', :reassign_to_id => 2
3050 delete :destroy, :ids => [1, 3], :todo => 'reassign', :reassign_to_id => 2
2922 end
3051 end
2923 end
3052 end
2924 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
3053 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
2925 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
3054 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
2926 assert_equal 2, TimeEntry.find(1).issue_id
3055 assert_equal 2, TimeEntry.find(1).issue_id
2927 assert_equal 2, TimeEntry.find(2).issue_id
3056 assert_equal 2, TimeEntry.find(2).issue_id
2928 end
3057 end
2929
3058
2930 def test_destroy_issues_from_different_projects
3059 def test_destroy_issues_from_different_projects
2931 @request.session[:user_id] = 2
3060 @request.session[:user_id] = 2
2932
3061
2933 assert_difference 'Issue.count', -3 do
3062 assert_difference 'Issue.count', -3 do
2934 delete :destroy, :ids => [1, 2, 6], :todo => 'destroy'
3063 delete :destroy, :ids => [1, 2, 6], :todo => 'destroy'
2935 end
3064 end
2936 assert_redirected_to :controller => 'issues', :action => 'index'
3065 assert_redirected_to :controller => 'issues', :action => 'index'
2937 assert !(Issue.find_by_id(1) || Issue.find_by_id(2) || Issue.find_by_id(6))
3066 assert !(Issue.find_by_id(1) || Issue.find_by_id(2) || Issue.find_by_id(6))
2938 end
3067 end
2939
3068
2940 def test_destroy_parent_and_child_issues
3069 def test_destroy_parent_and_child_issues
2941 parent = Issue.generate!(:project_id => 1, :tracker_id => 1)
3070 parent = Issue.generate!(:project_id => 1, :tracker_id => 1)
2942 child = Issue.generate!(:project_id => 1, :tracker_id => 1, :parent_issue_id => parent.id)
3071 child = Issue.generate!(:project_id => 1, :tracker_id => 1, :parent_issue_id => parent.id)
2943 assert child.is_descendant_of?(parent.reload)
3072 assert child.is_descendant_of?(parent.reload)
2944
3073
2945 @request.session[:user_id] = 2
3074 @request.session[:user_id] = 2
2946 assert_difference 'Issue.count', -2 do
3075 assert_difference 'Issue.count', -2 do
2947 delete :destroy, :ids => [parent.id, child.id], :todo => 'destroy'
3076 delete :destroy, :ids => [parent.id, child.id], :todo => 'destroy'
2948 end
3077 end
2949 assert_response 302
3078 assert_response 302
2950 end
3079 end
2951
3080
2952 def test_default_search_scope
3081 def test_default_search_scope
2953 get :index
3082 get :index
2954 assert_tag :div, :attributes => {:id => 'quick-search'},
3083 assert_tag :div, :attributes => {:id => 'quick-search'},
2955 :child => {:tag => 'form',
3084 :child => {:tag => 'form',
2956 :child => {:tag => 'input', :attributes => {:name => 'issues', :type => 'hidden', :value => '1'}}}
3085 :child => {:tag => 'input', :attributes => {:name => 'issues', :type => 'hidden', :value => '1'}}}
2957 end
3086 end
2958 end
3087 end
@@ -1,207 +1,232
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 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.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19 require 'issues_controller'
19 require 'issues_controller'
20
20
21 class IssuesControllerTransactionTest < ActionController::TestCase
21 class IssuesControllerTransactionTest < ActionController::TestCase
22 fixtures :projects,
22 fixtures :projects,
23 :users,
23 :users,
24 :roles,
24 :roles,
25 :members,
25 :members,
26 :member_roles,
26 :member_roles,
27 :issues,
27 :issues,
28 :issue_statuses,
28 :issue_statuses,
29 :versions,
29 :versions,
30 :trackers,
30 :trackers,
31 :projects_trackers,
31 :projects_trackers,
32 :issue_categories,
32 :issue_categories,
33 :enabled_modules,
33 :enabled_modules,
34 :enumerations,
34 :enumerations,
35 :attachments,
35 :attachments,
36 :workflows,
36 :workflows,
37 :custom_fields,
37 :custom_fields,
38 :custom_values,
38 :custom_values,
39 :custom_fields_projects,
39 :custom_fields_projects,
40 :custom_fields_trackers,
40 :custom_fields_trackers,
41 :time_entries,
41 :time_entries,
42 :journals,
42 :journals,
43 :journal_details,
43 :journal_details,
44 :queries
44 :queries
45
45
46 self.use_transactional_fixtures = false
46 self.use_transactional_fixtures = false
47
47
48 def setup
48 def setup
49 @controller = IssuesController.new
49 @controller = IssuesController.new
50 @request = ActionController::TestRequest.new
50 @request = ActionController::TestRequest.new
51 @response = ActionController::TestResponse.new
51 @response = ActionController::TestResponse.new
52 User.current = nil
52 User.current = nil
53 end
53 end
54
54
55 def test_update_stale_issue_should_not_update_the_issue
55 def test_update_stale_issue_should_not_update_the_issue
56 issue = Issue.find(2)
56 issue = Issue.find(2)
57 @request.session[:user_id] = 2
57 @request.session[:user_id] = 2
58
58
59 assert_no_difference 'Journal.count' do
59 assert_no_difference 'Journal.count' do
60 assert_no_difference 'TimeEntry.count' do
60 assert_no_difference 'TimeEntry.count' do
61 assert_no_difference 'Attachment.count' do
61 put :update,
62 :id => issue.id,
63 :issue => {
64 :fixed_version_id => 4,
65 :lock_version => (issue.lock_version - 1)
66 },
67 :notes => 'My notes',
68 :time_entry => { :hours => '2.5', :comments => '', :activity_id => TimeEntryActivity.first.id }
69 end
70 end
71
72 assert_response :success
73 assert_template 'edit'
74 assert_tag 'div', :attributes => {:class => 'conflict'}
75 assert_tag 'input', :attributes => {:name => 'conflict_resolution', :value => 'overwrite'}
76 assert_tag 'input', :attributes => {:name => 'conflict_resolution', :value => 'add_notes'}
77 assert_tag 'input', :attributes => {:name => 'conflict_resolution', :value => 'cancel'}
78 end
79
80 def test_update_stale_issue_should_save_attachments
81 set_tmp_attachments_directory
82 issue = Issue.find(2)
83 @request.session[:user_id] = 2
84
85 assert_no_difference 'Journal.count' do
86 assert_no_difference 'TimeEntry.count' do
87 assert_difference 'Attachment.count' do
62 put :update,
88 put :update,
63 :id => issue.id,
89 :id => issue.id,
64 :issue => {
90 :issue => {
65 :fixed_version_id => 4,
91 :fixed_version_id => 4,
66 :lock_version => (issue.lock_version - 1)
92 :lock_version => (issue.lock_version - 1)
67 },
93 },
68 :notes => 'My notes',
94 :notes => 'My notes',
69 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}},
95 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}},
70 :time_entry => { :hours => '2.5', :comments => '', :activity_id => TimeEntryActivity.first.id }
96 :time_entry => { :hours => '2.5', :comments => '', :activity_id => TimeEntryActivity.first.id }
71 end
97 end
72 end
98 end
73 end
99 end
74
100
75 assert_response :success
101 assert_response :success
76 assert_template 'edit'
102 assert_template 'edit'
77 assert_tag 'div', :attributes => {:class => 'conflict'}
103 attachment = Attachment.first(:order => 'id DESC')
78 assert_tag 'input', :attributes => {:name => 'conflict_resolution', :value => 'overwrite'}
104 assert_tag 'input', :attributes => {:name => 'attachments[p0][token]', :value => attachment.token}
79 assert_tag 'input', :attributes => {:name => 'conflict_resolution', :value => 'add_notes'}
105 assert_tag 'span', :content => /testfile.txt/
80 assert_tag 'input', :attributes => {:name => 'conflict_resolution', :value => 'cancel'}
81 end
106 end
82
107
83 def test_update_stale_issue_without_notes_should_not_show_add_notes_option
108 def test_update_stale_issue_without_notes_should_not_show_add_notes_option
84 issue = Issue.find(2)
109 issue = Issue.find(2)
85 @request.session[:user_id] = 2
110 @request.session[:user_id] = 2
86
111
87 put :update, :id => issue.id,
112 put :update, :id => issue.id,
88 :issue => {
113 :issue => {
89 :fixed_version_id => 4,
114 :fixed_version_id => 4,
90 :lock_version => (issue.lock_version - 1)
115 :lock_version => (issue.lock_version - 1)
91 },
116 },
92 :notes => ''
117 :notes => ''
93
118
94 assert_tag 'div', :attributes => {:class => 'conflict'}
119 assert_tag 'div', :attributes => {:class => 'conflict'}
95 assert_tag 'input', :attributes => {:name => 'conflict_resolution', :value => 'overwrite'}
120 assert_tag 'input', :attributes => {:name => 'conflict_resolution', :value => 'overwrite'}
96 assert_no_tag 'input', :attributes => {:name => 'conflict_resolution', :value => 'add_notes'}
121 assert_no_tag 'input', :attributes => {:name => 'conflict_resolution', :value => 'add_notes'}
97 assert_tag 'input', :attributes => {:name => 'conflict_resolution', :value => 'cancel'}
122 assert_tag 'input', :attributes => {:name => 'conflict_resolution', :value => 'cancel'}
98
123
99 end
124 end
100
125
101 def test_update_stale_issue_should_show_conflicting_journals
126 def test_update_stale_issue_should_show_conflicting_journals
102 @request.session[:user_id] = 2
127 @request.session[:user_id] = 2
103
128
104 put :update, :id => 1,
129 put :update, :id => 1,
105 :issue => {
130 :issue => {
106 :fixed_version_id => 4,
131 :fixed_version_id => 4,
107 :lock_version => 2
132 :lock_version => 2
108 },
133 },
109 :notes => '',
134 :notes => '',
110 :last_journal_id => 1
135 :last_journal_id => 1
111
136
112 assert_not_nil assigns(:conflict_journals)
137 assert_not_nil assigns(:conflict_journals)
113 assert_equal 1, assigns(:conflict_journals).size
138 assert_equal 1, assigns(:conflict_journals).size
114 assert_equal 2, assigns(:conflict_journals).first.id
139 assert_equal 2, assigns(:conflict_journals).first.id
115 assert_tag 'div', :attributes => {:class => 'conflict'},
140 assert_tag 'div', :attributes => {:class => 'conflict'},
116 :descendant => {:content => /Some notes with Redmine links/}
141 :descendant => {:content => /Some notes with Redmine links/}
117 end
142 end
118
143
119 def test_update_stale_issue_without_previous_journal_should_show_all_journals
144 def test_update_stale_issue_without_previous_journal_should_show_all_journals
120 @request.session[:user_id] = 2
145 @request.session[:user_id] = 2
121
146
122 put :update, :id => 1,
147 put :update, :id => 1,
123 :issue => {
148 :issue => {
124 :fixed_version_id => 4,
149 :fixed_version_id => 4,
125 :lock_version => 2
150 :lock_version => 2
126 },
151 },
127 :notes => '',
152 :notes => '',
128 :last_journal_id => ''
153 :last_journal_id => ''
129
154
130 assert_not_nil assigns(:conflict_journals)
155 assert_not_nil assigns(:conflict_journals)
131 assert_equal 2, assigns(:conflict_journals).size
156 assert_equal 2, assigns(:conflict_journals).size
132 assert_tag 'div', :attributes => {:class => 'conflict'},
157 assert_tag 'div', :attributes => {:class => 'conflict'},
133 :descendant => {:content => /Some notes with Redmine links/}
158 :descendant => {:content => /Some notes with Redmine links/}
134 assert_tag 'div', :attributes => {:class => 'conflict'},
159 assert_tag 'div', :attributes => {:class => 'conflict'},
135 :descendant => {:content => /Journal notes/}
160 :descendant => {:content => /Journal notes/}
136 end
161 end
137
162
138 def test_update_stale_issue_with_overwrite_conflict_resolution_should_update
163 def test_update_stale_issue_with_overwrite_conflict_resolution_should_update
139 @request.session[:user_id] = 2
164 @request.session[:user_id] = 2
140
165
141 assert_difference 'Journal.count' do
166 assert_difference 'Journal.count' do
142 put :update, :id => 1,
167 put :update, :id => 1,
143 :issue => {
168 :issue => {
144 :fixed_version_id => 4,
169 :fixed_version_id => 4,
145 :lock_version => 2
170 :lock_version => 2
146 },
171 },
147 :notes => 'overwrite_conflict_resolution',
172 :notes => 'overwrite_conflict_resolution',
148 :conflict_resolution => 'overwrite'
173 :conflict_resolution => 'overwrite'
149 end
174 end
150
175
151 assert_response 302
176 assert_response 302
152 issue = Issue.find(1)
177 issue = Issue.find(1)
153 assert_equal 4, issue.fixed_version_id
178 assert_equal 4, issue.fixed_version_id
154 journal = Journal.first(:order => 'id DESC')
179 journal = Journal.first(:order => 'id DESC')
155 assert_equal 'overwrite_conflict_resolution', journal.notes
180 assert_equal 'overwrite_conflict_resolution', journal.notes
156 assert journal.details.any?
181 assert journal.details.any?
157 end
182 end
158
183
159 def test_update_stale_issue_with_add_notes_conflict_resolution_should_update
184 def test_update_stale_issue_with_add_notes_conflict_resolution_should_update
160 @request.session[:user_id] = 2
185 @request.session[:user_id] = 2
161
186
162 assert_difference 'Journal.count' do
187 assert_difference 'Journal.count' do
163 put :update, :id => 1,
188 put :update, :id => 1,
164 :issue => {
189 :issue => {
165 :fixed_version_id => 4,
190 :fixed_version_id => 4,
166 :lock_version => 2
191 :lock_version => 2
167 },
192 },
168 :notes => 'add_notes_conflict_resolution',
193 :notes => 'add_notes_conflict_resolution',
169 :conflict_resolution => 'add_notes'
194 :conflict_resolution => 'add_notes'
170 end
195 end
171
196
172 assert_response 302
197 assert_response 302
173 issue = Issue.find(1)
198 issue = Issue.find(1)
174 assert_nil issue.fixed_version_id
199 assert_nil issue.fixed_version_id
175 journal = Journal.first(:order => 'id DESC')
200 journal = Journal.first(:order => 'id DESC')
176 assert_equal 'add_notes_conflict_resolution', journal.notes
201 assert_equal 'add_notes_conflict_resolution', journal.notes
177 assert journal.details.empty?
202 assert journal.details.empty?
178 end
203 end
179
204
180 def test_update_stale_issue_with_cancel_conflict_resolution_should_redirect_without_updating
205 def test_update_stale_issue_with_cancel_conflict_resolution_should_redirect_without_updating
181 @request.session[:user_id] = 2
206 @request.session[:user_id] = 2
182
207
183 assert_no_difference 'Journal.count' do
208 assert_no_difference 'Journal.count' do
184 put :update, :id => 1,
209 put :update, :id => 1,
185 :issue => {
210 :issue => {
186 :fixed_version_id => 4,
211 :fixed_version_id => 4,
187 :lock_version => 2
212 :lock_version => 2
188 },
213 },
189 :notes => 'add_notes_conflict_resolution',
214 :notes => 'add_notes_conflict_resolution',
190 :conflict_resolution => 'cancel'
215 :conflict_resolution => 'cancel'
191 end
216 end
192
217
193 assert_redirected_to '/issues/1'
218 assert_redirected_to '/issues/1'
194 issue = Issue.find(1)
219 issue = Issue.find(1)
195 assert_nil issue.fixed_version_id
220 assert_nil issue.fixed_version_id
196 end
221 end
197
222
198 def test_index_should_rescue_invalid_sql_query
223 def test_index_should_rescue_invalid_sql_query
199 Query.any_instance.stubs(:statement).returns("INVALID STATEMENT")
224 Query.any_instance.stubs(:statement).returns("INVALID STATEMENT")
200
225
201 get :index
226 get :index
202 assert_response 500
227 assert_response 500
203 assert_tag 'p', :content => /An error occurred/
228 assert_tag 'p', :content => /An error occurred/
204 assert_nil session[:query]
229 assert_nil session[:query]
205 assert_nil session[:issues_index_sort]
230 assert_nil session[:issues_index_sort]
206 end
231 end
207 end
232 end
@@ -1,203 +1,207
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2011 Jean-Philippe Lang
4 # Copyright (C) 2006-2011 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 File.expand_path('../../test_helper', __FILE__)
20 require File.expand_path('../../test_helper', __FILE__)
21
21
22 class AttachmentTest < ActiveSupport::TestCase
22 class AttachmentTest < ActiveSupport::TestCase
23 fixtures :users, :projects, :roles, :members, :member_roles,
23 fixtures :users, :projects, :roles, :members, :member_roles,
24 :enabled_modules, :issues, :trackers, :attachments
24 :enabled_modules, :issues, :trackers, :attachments
25
25
26 class MockFile
26 class MockFile
27 attr_reader :original_filename, :content_type, :content, :size
27 attr_reader :original_filename, :content_type, :content, :size
28
28
29 def initialize(attributes)
29 def initialize(attributes)
30 @original_filename = attributes[:original_filename]
30 @original_filename = attributes[:original_filename]
31 @content_type = attributes[:content_type]
31 @content_type = attributes[:content_type]
32 @content = attributes[:content] || "Content"
32 @content = attributes[:content] || "Content"
33 @size = content.size
33 @size = content.size
34 end
34 end
35 end
35 end
36
36
37 def setup
37 def setup
38 set_tmp_attachments_directory
38 set_tmp_attachments_directory
39 end
39 end
40
40
41 def test_container_for_new_attachment_should_be_nil
42 assert_nil Attachment.new.container
43 end
44
41 def test_create
45 def test_create
42 a = Attachment.new(:container => Issue.find(1),
46 a = Attachment.new(:container => Issue.find(1),
43 :file => uploaded_test_file("testfile.txt", "text/plain"),
47 :file => uploaded_test_file("testfile.txt", "text/plain"),
44 :author => User.find(1))
48 :author => User.find(1))
45 assert a.save
49 assert a.save
46 assert_equal 'testfile.txt', a.filename
50 assert_equal 'testfile.txt', a.filename
47 assert_equal 59, a.filesize
51 assert_equal 59, a.filesize
48 assert_equal 'text/plain', a.content_type
52 assert_equal 'text/plain', a.content_type
49 assert_equal 0, a.downloads
53 assert_equal 0, a.downloads
50 assert_equal '1478adae0d4eb06d35897518540e25d6', a.digest
54 assert_equal '1478adae0d4eb06d35897518540e25d6', a.digest
51 assert File.exist?(a.diskfile)
55 assert File.exist?(a.diskfile)
52 assert_equal 59, File.size(a.diskfile)
56 assert_equal 59, File.size(a.diskfile)
53 end
57 end
54
58
55 def test_size_should_be_validated_for_new_file
59 def test_size_should_be_validated_for_new_file
56 with_settings :attachment_max_size => 0 do
60 with_settings :attachment_max_size => 0 do
57 a = Attachment.new(:container => Issue.find(1),
61 a = Attachment.new(:container => Issue.find(1),
58 :file => uploaded_test_file("testfile.txt", "text/plain"),
62 :file => uploaded_test_file("testfile.txt", "text/plain"),
59 :author => User.find(1))
63 :author => User.find(1))
60 assert !a.save
64 assert !a.save
61 end
65 end
62 end
66 end
63
67
64 def test_size_should_not_be_validated_when_copying
68 def test_size_should_not_be_validated_when_copying
65 a = Attachment.create!(:container => Issue.find(1),
69 a = Attachment.create!(:container => Issue.find(1),
66 :file => uploaded_test_file("testfile.txt", "text/plain"),
70 :file => uploaded_test_file("testfile.txt", "text/plain"),
67 :author => User.find(1))
71 :author => User.find(1))
68 with_settings :attachment_max_size => 0 do
72 with_settings :attachment_max_size => 0 do
69 copy = a.copy
73 copy = a.copy
70 assert copy.save
74 assert copy.save
71 end
75 end
72 end
76 end
73
77
74 def test_destroy
78 def test_destroy
75 a = Attachment.new(:container => Issue.find(1),
79 a = Attachment.new(:container => Issue.find(1),
76 :file => uploaded_test_file("testfile.txt", "text/plain"),
80 :file => uploaded_test_file("testfile.txt", "text/plain"),
77 :author => User.find(1))
81 :author => User.find(1))
78 assert a.save
82 assert a.save
79 assert_equal 'testfile.txt', a.filename
83 assert_equal 'testfile.txt', a.filename
80 assert_equal 59, a.filesize
84 assert_equal 59, a.filesize
81 assert_equal 'text/plain', a.content_type
85 assert_equal 'text/plain', a.content_type
82 assert_equal 0, a.downloads
86 assert_equal 0, a.downloads
83 assert_equal '1478adae0d4eb06d35897518540e25d6', a.digest
87 assert_equal '1478adae0d4eb06d35897518540e25d6', a.digest
84 diskfile = a.diskfile
88 diskfile = a.diskfile
85 assert File.exist?(diskfile)
89 assert File.exist?(diskfile)
86 assert_equal 59, File.size(a.diskfile)
90 assert_equal 59, File.size(a.diskfile)
87 assert a.destroy
91 assert a.destroy
88 assert !File.exist?(diskfile)
92 assert !File.exist?(diskfile)
89 end
93 end
90
94
91 def test_destroy_should_not_delete_file_referenced_by_other_attachment
95 def test_destroy_should_not_delete_file_referenced_by_other_attachment
92 a = Attachment.create!(:container => Issue.find(1),
96 a = Attachment.create!(:container => Issue.find(1),
93 :file => uploaded_test_file("testfile.txt", "text/plain"),
97 :file => uploaded_test_file("testfile.txt", "text/plain"),
94 :author => User.find(1))
98 :author => User.find(1))
95 diskfile = a.diskfile
99 diskfile = a.diskfile
96
100
97 copy = a.copy
101 copy = a.copy
98 copy.save!
102 copy.save!
99
103
100 assert File.exists?(diskfile)
104 assert File.exists?(diskfile)
101 a.destroy
105 a.destroy
102 assert File.exists?(diskfile)
106 assert File.exists?(diskfile)
103 copy.destroy
107 copy.destroy
104 assert !File.exists?(diskfile)
108 assert !File.exists?(diskfile)
105 end
109 end
106
110
107 def test_create_should_auto_assign_content_type
111 def test_create_should_auto_assign_content_type
108 a = Attachment.new(:container => Issue.find(1),
112 a = Attachment.new(:container => Issue.find(1),
109 :file => uploaded_test_file("testfile.txt", ""),
113 :file => uploaded_test_file("testfile.txt", ""),
110 :author => User.find(1))
114 :author => User.find(1))
111 assert a.save
115 assert a.save
112 assert_equal 'text/plain', a.content_type
116 assert_equal 'text/plain', a.content_type
113 end
117 end
114
118
115 def test_identical_attachments_at_the_same_time_should_not_overwrite
119 def test_identical_attachments_at_the_same_time_should_not_overwrite
116 a1 = Attachment.create!(:container => Issue.find(1),
120 a1 = Attachment.create!(:container => Issue.find(1),
117 :file => uploaded_test_file("testfile.txt", ""),
121 :file => uploaded_test_file("testfile.txt", ""),
118 :author => User.find(1))
122 :author => User.find(1))
119 a2 = Attachment.create!(:container => Issue.find(1),
123 a2 = Attachment.create!(:container => Issue.find(1),
120 :file => uploaded_test_file("testfile.txt", ""),
124 :file => uploaded_test_file("testfile.txt", ""),
121 :author => User.find(1))
125 :author => User.find(1))
122 assert a1.disk_filename != a2.disk_filename
126 assert a1.disk_filename != a2.disk_filename
123 end
127 end
124
128
125 def test_filename_should_be_basenamed
129 def test_filename_should_be_basenamed
126 a = Attachment.new(:file => MockFile.new(:original_filename => "path/to/the/file"))
130 a = Attachment.new(:file => MockFile.new(:original_filename => "path/to/the/file"))
127 assert_equal 'file', a.filename
131 assert_equal 'file', a.filename
128 end
132 end
129
133
130 def test_filename_should_be_sanitized
134 def test_filename_should_be_sanitized
131 a = Attachment.new(:file => MockFile.new(:original_filename => "valid:[] invalid:?%*|\"'<>chars"))
135 a = Attachment.new(:file => MockFile.new(:original_filename => "valid:[] invalid:?%*|\"'<>chars"))
132 assert_equal 'valid_[] invalid_chars', a.filename
136 assert_equal 'valid_[] invalid_chars', a.filename
133 end
137 end
134
138
135 def test_diskfilename
139 def test_diskfilename
136 assert Attachment.disk_filename("test_file.txt") =~ /^\d{12}_test_file.txt$/
140 assert Attachment.disk_filename("test_file.txt") =~ /^\d{12}_test_file.txt$/
137 assert_equal 'test_file.txt', Attachment.disk_filename("test_file.txt")[13..-1]
141 assert_equal 'test_file.txt', Attachment.disk_filename("test_file.txt")[13..-1]
138 assert_equal '770c509475505f37c2b8fb6030434d6b.txt', Attachment.disk_filename("test_accentuΓ©.txt")[13..-1]
142 assert_equal '770c509475505f37c2b8fb6030434d6b.txt', Attachment.disk_filename("test_accentuΓ©.txt")[13..-1]
139 assert_equal 'f8139524ebb8f32e51976982cd20a85d', Attachment.disk_filename("test_accentuΓ©")[13..-1]
143 assert_equal 'f8139524ebb8f32e51976982cd20a85d', Attachment.disk_filename("test_accentuΓ©")[13..-1]
140 assert_equal 'cbb5b0f30978ba03731d61f9f6d10011', Attachment.disk_filename("test_accentuΓ©.Γ§a")[13..-1]
144 assert_equal 'cbb5b0f30978ba03731d61f9f6d10011', Attachment.disk_filename("test_accentuΓ©.Γ§a")[13..-1]
141 end
145 end
142
146
143 context "Attachmnet.attach_files" do
147 context "Attachmnet.attach_files" do
144 should "attach the file" do
148 should "attach the file" do
145 issue = Issue.first
149 issue = Issue.first
146 assert_difference 'Attachment.count' do
150 assert_difference 'Attachment.count' do
147 Attachment.attach_files(issue,
151 Attachment.attach_files(issue,
148 '1' => {
152 '1' => {
149 'file' => uploaded_test_file('testfile.txt', 'text/plain'),
153 'file' => uploaded_test_file('testfile.txt', 'text/plain'),
150 'description' => 'test'
154 'description' => 'test'
151 })
155 })
152 end
156 end
153
157
154 attachment = Attachment.first(:order => 'id DESC')
158 attachment = Attachment.first(:order => 'id DESC')
155 assert_equal issue, attachment.container
159 assert_equal issue, attachment.container
156 assert_equal 'testfile.txt', attachment.filename
160 assert_equal 'testfile.txt', attachment.filename
157 assert_equal 59, attachment.filesize
161 assert_equal 59, attachment.filesize
158 assert_equal 'test', attachment.description
162 assert_equal 'test', attachment.description
159 assert_equal 'text/plain', attachment.content_type
163 assert_equal 'text/plain', attachment.content_type
160 assert File.exists?(attachment.diskfile)
164 assert File.exists?(attachment.diskfile)
161 assert_equal 59, File.size(attachment.diskfile)
165 assert_equal 59, File.size(attachment.diskfile)
162 end
166 end
163
167
164 should "add unsaved files to the object as unsaved attachments" do
168 should "add unsaved files to the object as unsaved attachments" do
165 # Max size of 0 to force Attachment creation failures
169 # Max size of 0 to force Attachment creation failures
166 with_settings(:attachment_max_size => 0) do
170 with_settings(:attachment_max_size => 0) do
167 @project = Project.generate!
171 @project = Project.generate!
168 response = Attachment.attach_files(@project, {
172 response = Attachment.attach_files(@project, {
169 '1' => {'file' => mock_file, 'description' => 'test'},
173 '1' => {'file' => mock_file, 'description' => 'test'},
170 '2' => {'file' => mock_file, 'description' => 'test'}
174 '2' => {'file' => mock_file, 'description' => 'test'}
171 })
175 })
172
176
173 assert response[:unsaved].present?
177 assert response[:unsaved].present?
174 assert_equal 2, response[:unsaved].length
178 assert_equal 2, response[:unsaved].length
175 assert response[:unsaved].first.new_record?
179 assert response[:unsaved].first.new_record?
176 assert response[:unsaved].second.new_record?
180 assert response[:unsaved].second.new_record?
177 assert_equal response[:unsaved], @project.unsaved_attachments
181 assert_equal response[:unsaved], @project.unsaved_attachments
178 end
182 end
179 end
183 end
180 end
184 end
181
185
182 def test_latest_attach
186 def test_latest_attach
183 set_fixtures_attachments_directory
187 set_fixtures_attachments_directory
184 a1 = Attachment.find(16)
188 a1 = Attachment.find(16)
185 assert_equal "testfile.png", a1.filename
189 assert_equal "testfile.png", a1.filename
186 assert a1.readable?
190 assert a1.readable?
187 assert (! a1.visible?(User.anonymous))
191 assert (! a1.visible?(User.anonymous))
188 assert a1.visible?(User.find(2))
192 assert a1.visible?(User.find(2))
189 a2 = Attachment.find(17)
193 a2 = Attachment.find(17)
190 assert_equal "testfile.PNG", a2.filename
194 assert_equal "testfile.PNG", a2.filename
191 assert a2.readable?
195 assert a2.readable?
192 assert (! a2.visible?(User.anonymous))
196 assert (! a2.visible?(User.anonymous))
193 assert a2.visible?(User.find(2))
197 assert a2.visible?(User.find(2))
194 assert a1.created_on < a2.created_on
198 assert a1.created_on < a2.created_on
195
199
196 la1 = Attachment.latest_attach([a1, a2], "testfile.png")
200 la1 = Attachment.latest_attach([a1, a2], "testfile.png")
197 assert_equal 17, la1.id
201 assert_equal 17, la1.id
198 la2 = Attachment.latest_attach([a1, a2], "Testfile.PNG")
202 la2 = Attachment.latest_attach([a1, a2], "Testfile.PNG")
199 assert_equal 17, la2.id
203 assert_equal 17, la2.id
200
204
201 set_tmp_attachments_directory
205 set_tmp_attachments_directory
202 end
206 end
203 end
207 end
@@ -1,60 +1,97
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 module Redmine
18 module Redmine
19 module Acts
19 module Acts
20 module Attachable
20 module Attachable
21 def self.included(base)
21 def self.included(base)
22 base.extend ClassMethods
22 base.extend ClassMethods
23 end
23 end
24
24
25 module ClassMethods
25 module ClassMethods
26 def acts_as_attachable(options = {})
26 def acts_as_attachable(options = {})
27 cattr_accessor :attachable_options
27 cattr_accessor :attachable_options
28 self.attachable_options = {}
28 self.attachable_options = {}
29 attachable_options[:view_permission] = options.delete(:view_permission) || "view_#{self.name.pluralize.underscore}".to_sym
29 attachable_options[:view_permission] = options.delete(:view_permission) || "view_#{self.name.pluralize.underscore}".to_sym
30 attachable_options[:delete_permission] = options.delete(:delete_permission) || "edit_#{self.name.pluralize.underscore}".to_sym
30 attachable_options[:delete_permission] = options.delete(:delete_permission) || "edit_#{self.name.pluralize.underscore}".to_sym
31
31
32 has_many :attachments, options.merge(:as => :container,
32 has_many :attachments, options.merge(:as => :container,
33 :order => "#{Attachment.table_name}.created_on",
33 :order => "#{Attachment.table_name}.created_on",
34 :dependent => :destroy)
34 :dependent => :destroy)
35 attr_accessor :unsaved_attachments
36 send :include, Redmine::Acts::Attachable::InstanceMethods
35 send :include, Redmine::Acts::Attachable::InstanceMethods
36 before_save :attach_saved_attachments
37 end
37 end
38 end
38 end
39
39
40 module InstanceMethods
40 module InstanceMethods
41 def self.included(base)
41 def self.included(base)
42 base.extend ClassMethods
42 base.extend ClassMethods
43 end
43 end
44
44
45 def attachments_visible?(user=User.current)
45 def attachments_visible?(user=User.current)
46 (respond_to?(:visible?) ? visible?(user) : true) &&
46 (respond_to?(:visible?) ? visible?(user) : true) &&
47 user.allowed_to?(self.class.attachable_options[:view_permission], self.project)
47 user.allowed_to?(self.class.attachable_options[:view_permission], self.project)
48 end
48 end
49
49
50 def attachments_deletable?(user=User.current)
50 def attachments_deletable?(user=User.current)
51 (respond_to?(:visible?) ? visible?(user) : true) &&
51 (respond_to?(:visible?) ? visible?(user) : true) &&
52 user.allowed_to?(self.class.attachable_options[:delete_permission], self.project)
52 user.allowed_to?(self.class.attachable_options[:delete_permission], self.project)
53 end
53 end
54
54
55 def saved_attachments
56 @saved_attachments ||= []
57 end
58
59 def unsaved_attachments
60 @unsaved_attachments ||= []
61 end
62
63 def save_attachments(attachments, author=User.current)
64 if attachments && attachments.is_a?(Hash)
65 attachments.each_value do |attachment|
66 a = nil
67 if file = attachment['file']
68 next unless file && file.size > 0
69 a = Attachment.create(:file => file,
70 :description => attachment['description'].to_s.strip,
71 :author => author)
72 elsif token = attachment['token']
73 a = Attachment.find_by_token(token)
74 end
75 next unless a
76 if a.new_record?
77 unsaved_attachments << a
78 else
79 saved_attachments << a
80 end
81 end
82 end
83 {:files => saved_attachments, :unsaved => unsaved_attachments}
84 end
85
86 def attach_saved_attachments
87 saved_attachments.each do |attachment|
88 self.attachments << attachment
89 end
90 end
91
55 module ClassMethods
92 module ClassMethods
56 end
93 end
57 end
94 end
58 end
95 end
59 end
96 end
60 end
97 end
General Comments 0
You need to be logged in to leave comments. Login now