##// 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 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class IssuesController < ApplicationController
19 19 menu_item :new_issue, :only => [:new, :create]
20 20 default_search_scope :issues
21 21
22 22 before_filter :find_issue, :only => [:show, :edit, :update]
23 23 before_filter :find_issues, :only => [:bulk_edit, :bulk_update, :move, :perform_move, :destroy]
24 24 before_filter :check_project_uniqueness, :only => [:move, :perform_move]
25 25 before_filter :find_project, :only => [:new, :create]
26 26 before_filter :authorize, :except => [:index]
27 27 before_filter :find_optional_project, :only => [:index]
28 28 before_filter :check_for_default_issue_status, :only => [:new, :create]
29 29 before_filter :build_new_issue_from_params, :only => [:new, :create]
30 30 accept_rss_auth :index, :show
31 31 accept_api_auth :index, :show, :create, :update, :destroy
32 32
33 33 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
34 34
35 35 helper :journals
36 36 helper :projects
37 37 include ProjectsHelper
38 38 helper :custom_fields
39 39 include CustomFieldsHelper
40 40 helper :issue_relations
41 41 include IssueRelationsHelper
42 42 helper :watchers
43 43 include WatchersHelper
44 44 helper :attachments
45 45 include AttachmentsHelper
46 46 helper :queries
47 47 include QueriesHelper
48 48 helper :repositories
49 49 include RepositoriesHelper
50 50 helper :sort
51 51 include SortHelper
52 52 include IssuesHelper
53 53 helper :timelog
54 54 helper :gantt
55 55 include Redmine::Export::PDF
56 56
57 57 verify :method => :post, :only => :create, :render => {:nothing => true, :status => :method_not_allowed }
58 58 verify :method => :post, :only => :bulk_update, :render => {:nothing => true, :status => :method_not_allowed }
59 59 verify :method => :put, :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
60 60
61 61 def index
62 62 retrieve_query
63 63 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
64 64 sort_update(@query.sortable_columns)
65 65
66 66 if @query.valid?
67 67 case params[:format]
68 68 when 'csv', 'pdf'
69 69 @limit = Setting.issues_export_limit.to_i
70 70 when 'atom'
71 71 @limit = Setting.feeds_limit.to_i
72 72 when 'xml', 'json'
73 73 @offset, @limit = api_offset_and_limit
74 74 else
75 75 @limit = per_page_option
76 76 end
77 77
78 78 @issue_count = @query.issue_count
79 79 @issue_pages = Paginator.new self, @issue_count, @limit, params['page']
80 80 @offset ||= @issue_pages.current.offset
81 81 @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
82 82 :order => sort_clause,
83 83 :offset => @offset,
84 84 :limit => @limit)
85 85 @issue_count_by_group = @query.issue_count_by_group
86 86
87 87 respond_to do |format|
88 88 format.html { render :template => 'issues/index', :layout => !request.xhr? }
89 89 format.api {
90 90 Issue.load_relations(@issues) if include_in_api_response?('relations')
91 91 }
92 92 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
93 93 format.csv { send_data(issues_to_csv(@issues, @project, @query, params), :type => 'text/csv; header=present', :filename => 'export.csv') }
94 94 format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'export.pdf') }
95 95 end
96 96 else
97 97 respond_to do |format|
98 98 format.html { render(:template => 'issues/index', :layout => !request.xhr?) }
99 99 format.any(:atom, :csv, :pdf) { render(:nothing => true) }
100 100 format.api { render_validation_errors(@query) }
101 101 end
102 102 end
103 103 rescue ActiveRecord::RecordNotFound
104 104 render_404
105 105 end
106 106
107 107 def show
108 108 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
109 109 @journals.each_with_index {|j,i| j.indice = i+1}
110 110 @journals.reverse! if User.current.wants_comments_in_reverse_order?
111 111
112 112 @changesets = @issue.changesets.visible.all
113 113 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
114 114
115 115 @relations = @issue.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
116 116 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
117 117 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
118 118 @priorities = IssuePriority.active
119 119 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
120 120 respond_to do |format|
121 121 format.html {
122 122 retrieve_previous_and_next_issue_ids
123 123 render :template => 'issues/show'
124 124 }
125 125 format.api
126 126 format.atom { render :template => 'journals/index', :layout => false, :content_type => 'application/atom+xml' }
127 127 format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
128 128 end
129 129 end
130 130
131 131 # Add a new issue
132 132 # The new issue will be created from an existing one if copy_from parameter is given
133 133 def new
134 134 respond_to do |format|
135 135 format.html { render :action => 'new', :layout => !request.xhr? }
136 136 format.js {
137 137 render(:update) { |page|
138 138 if params[:project_change]
139 139 page.replace_html 'all_attributes', :partial => 'form'
140 140 else
141 141 page.replace_html 'attributes', :partial => 'attributes'
142 142 end
143 143 m = User.current.allowed_to?(:log_time, @issue.project) ? 'show' : 'hide'
144 144 page << "if ($('log_time')) {Element.#{m}('log_time');}"
145 145 }
146 146 }
147 147 end
148 148 end
149 149
150 150 def create
151 151 call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
152 @issue.save_attachments(params[:attachments])
152 153 if @issue.save
153 attachments = Attachment.attach_files(@issue, params[:attachments])
154 154 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
155 155 respond_to do |format|
156 156 format.html {
157 157 render_attachment_warning_if_needed(@issue)
158 158 flash[:notice] = l(:notice_issue_successful_create, :id => "<a href='#{issue_path(@issue)}'>##{@issue.id}</a>")
159 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 160 { :action => 'show', :id => @issue })
161 161 }
162 162 format.api { render :action => 'show', :status => :created, :location => issue_url(@issue) }
163 163 end
164 164 return
165 165 else
166 166 respond_to do |format|
167 167 format.html { render :action => 'new' }
168 168 format.api { render_validation_errors(@issue) }
169 169 end
170 170 end
171 171 end
172 172
173 173 def edit
174 174 return unless update_issue_from_params
175 175
176 176 respond_to do |format|
177 177 format.html { }
178 178 format.xml { }
179 179 end
180 180 end
181 181
182 182 def update
183 183 return unless update_issue_from_params
184 @issue.save_attachments(params[:attachments])
184 185 saved = false
185 186 begin
186 187 saved = @issue.save_issue_with_child_records(params, @time_entry)
187 188 rescue ActiveRecord::StaleObjectError
188 189 @conflict = true
189 190 if params[:last_journal_id]
190 191 if params[:last_journal_id].present?
191 192 last_journal_id = params[:last_journal_id].to_i
192 193 @conflict_journals = @issue.journals.all(:conditions => ["#{Journal.table_name}.id > ?", last_journal_id])
193 194 else
194 195 @conflict_journals = @issue.journals.all
195 196 end
196 197 end
197 198 end
198 199
199 200 if saved
200 201 render_attachment_warning_if_needed(@issue)
201 202 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
202 203
203 204 respond_to do |format|
204 205 format.html { redirect_back_or_default({:action => 'show', :id => @issue}) }
205 206 format.api { head :ok }
206 207 end
207 208 else
208 209 respond_to do |format|
209 210 format.html { render :action => 'edit' }
210 211 format.api { render_validation_errors(@issue) }
211 212 end
212 213 end
213 214 end
214 215
215 216 # Bulk edit/copy a set of issues
216 217 def bulk_edit
217 218 @issues.sort!
218 219 @copy = params[:copy].present?
219 220 @notes = params[:notes]
220 221
221 222 if User.current.allowed_to?(:move_issues, @projects)
222 223 @allowed_projects = Issue.allowed_target_projects_on_move
223 224 if params[:issue]
224 225 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:issue][:project_id]}
225 226 if @target_project
226 227 target_projects = [@target_project]
227 228 end
228 229 end
229 230 end
230 231 target_projects ||= @projects
231 232
232 233 @available_statuses = @issues.map(&:new_statuses_allowed_to).reduce(:&)
233 234 @custom_fields = target_projects.map{|p|p.all_issue_custom_fields}.reduce(:&)
234 235 @assignables = target_projects.map(&:assignable_users).reduce(:&)
235 236 @trackers = target_projects.map(&:trackers).reduce(:&)
236 237
237 238 @safe_attributes = @issues.map(&:safe_attribute_names).reduce(:&)
238 239 render :layout => false if request.xhr?
239 240 end
240 241
241 242 def bulk_update
242 243 @issues.sort!
243 244 @copy = params[:copy].present?
244 245 attributes = parse_params_for_bulk_issue_attributes(params)
245 246
246 247 unsaved_issue_ids = []
247 248 moved_issues = []
248 249 @issues.each do |issue|
249 250 issue.reload
250 251 if @copy
251 252 issue = issue.copy
252 253 end
253 254 journal = issue.init_journal(User.current, params[:notes])
254 255 issue.safe_attributes = attributes
255 256 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
256 257 if issue.save
257 258 moved_issues << issue
258 259 else
259 260 # Keep unsaved issue ids to display them in flash error
260 261 unsaved_issue_ids << issue.id
261 262 end
262 263 end
263 264 set_flash_from_bulk_issue_save(@issues, unsaved_issue_ids)
264 265
265 266 if params[:follow]
266 267 if @issues.size == 1 && moved_issues.size == 1
267 268 redirect_to :controller => 'issues', :action => 'show', :id => moved_issues.first
268 269 elsif moved_issues.map(&:project).uniq.size == 1
269 270 redirect_to :controller => 'issues', :action => 'index', :project_id => moved_issues.map(&:project).first
270 271 end
271 272 else
272 273 redirect_back_or_default({:controller => 'issues', :action => 'index', :project_id => @project})
273 274 end
274 275 end
275 276
276 277 verify :method => :delete, :only => :destroy, :render => { :nothing => true, :status => :method_not_allowed }
277 278 def destroy
278 279 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
279 280 if @hours > 0
280 281 case params[:todo]
281 282 when 'destroy'
282 283 # nothing to do
283 284 when 'nullify'
284 285 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
285 286 when 'reassign'
286 287 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
287 288 if reassign_to.nil?
288 289 flash.now[:error] = l(:error_issue_not_found_in_project)
289 290 return
290 291 else
291 292 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
292 293 end
293 294 else
294 295 # display the destroy form if it's a user request
295 296 return unless api_request?
296 297 end
297 298 end
298 299 @issues.each do |issue|
299 300 begin
300 301 issue.reload.destroy
301 302 rescue ::ActiveRecord::RecordNotFound # raised by #reload if issue no longer exists
302 303 # nothing to do, issue was already deleted (eg. by a parent)
303 304 end
304 305 end
305 306 respond_to do |format|
306 307 format.html { redirect_back_or_default(:action => 'index', :project_id => @project) }
307 308 format.api { head :ok }
308 309 end
309 310 end
310 311
311 312 private
312 313 def find_issue
313 314 # Issue.visible.find(...) can not be used to redirect user to the login form
314 315 # if the issue actually exists but requires authentication
315 316 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
316 317 unless @issue.visible?
317 318 deny_access
318 319 return
319 320 end
320 321 @project = @issue.project
321 322 rescue ActiveRecord::RecordNotFound
322 323 render_404
323 324 end
324 325
325 326 def find_project
326 327 project_id = params[:project_id] || (params[:issue] && params[:issue][:project_id])
327 328 @project = Project.find(project_id)
328 329 rescue ActiveRecord::RecordNotFound
329 330 render_404
330 331 end
331 332
332 333 def retrieve_previous_and_next_issue_ids
333 334 retrieve_query_from_session
334 335 if @query
335 336 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
336 337 sort_update(@query.sortable_columns, 'issues_index_sort')
337 338 limit = 500
338 339 issue_ids = @query.issue_ids(:order => sort_clause, :limit => (limit + 1), :include => [:assigned_to, :tracker, :priority, :category, :fixed_version])
339 340 if (idx = issue_ids.index(@issue.id)) && idx < limit
340 341 if issue_ids.size < 500
341 342 @issue_position = idx + 1
342 343 @issue_count = issue_ids.size
343 344 end
344 345 @prev_issue_id = issue_ids[idx - 1] if idx > 0
345 346 @next_issue_id = issue_ids[idx + 1] if idx < (issue_ids.size - 1)
346 347 end
347 348 end
348 349 end
349 350
350 351 # Used by #edit and #update to set some common instance variables
351 352 # from the params
352 353 # TODO: Refactor, not everything in here is needed by #edit
353 354 def update_issue_from_params
354 355 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
355 356 @priorities = IssuePriority.active
356 357 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
357 358 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
358 359 @time_entry.attributes = params[:time_entry]
359 360
360 361 @notes = params[:notes] || (params[:issue].present? ? params[:issue][:notes] : nil)
361 362 @issue.init_journal(User.current, @notes)
362 363
363 364 issue_attributes = params[:issue]
364 365 if issue_attributes && params[:conflict_resolution]
365 366 case params[:conflict_resolution]
366 367 when 'overwrite'
367 368 issue_attributes = issue_attributes.dup
368 369 issue_attributes.delete(:lock_version)
369 370 when 'add_notes'
370 371 issue_attributes = {}
371 372 when 'cancel'
372 373 redirect_to issue_path(@issue)
373 374 return false
374 375 end
375 376 end
376 377 @issue.safe_attributes = issue_attributes
377 378 true
378 379 end
379 380
380 381 # TODO: Refactor, lots of extra code in here
381 382 # TODO: Changing tracker on an existing issue should not trigger this
382 383 def build_new_issue_from_params
383 384 if params[:id].blank?
384 385 @issue = Issue.new
385 386 if params[:copy_from]
386 387 begin
387 388 @copy_from = Issue.visible.find(params[:copy_from])
388 389 @copy_attachments = params[:copy_attachments].present? || request.get?
389 390 @issue.copy_from(@copy_from, :attachments => @copy_attachments)
390 391 rescue ActiveRecord::RecordNotFound
391 392 render_404
392 393 return
393 394 end
394 395 end
395 396 @issue.project = @project
396 397 else
397 398 @issue = @project.issues.visible.find(params[:id])
398 399 end
399 400
400 401 @issue.project = @project
401 402 @issue.author = User.current
402 403 # Tracker must be set before custom field values
403 404 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
404 405 if @issue.tracker.nil?
405 406 render_error l(:error_no_tracker_in_project)
406 407 return false
407 408 end
408 409 @issue.start_date ||= Date.today if Setting.default_issue_start_date_to_creation_date?
409 410 @issue.safe_attributes = params[:issue]
410 411
411 412 @priorities = IssuePriority.active
412 413 @allowed_statuses = @issue.new_statuses_allowed_to(User.current, true)
413 414 end
414 415
415 416 def check_for_default_issue_status
416 417 if IssueStatus.default.nil?
417 418 render_error l(:error_no_default_issue_status)
418 419 return false
419 420 end
420 421 end
421 422
422 423 def parse_params_for_bulk_issue_attributes(params)
423 424 attributes = (params[:issue] || {}).reject {|k,v| v.blank?}
424 425 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
425 426 attributes[:custom_field_values].reject! {|k,v| v.blank?} if attributes[:custom_field_values]
426 427 attributes
427 428 end
428 429 end
@@ -1,219 +1,232
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require "digest/md5"
19 19
20 20 class Attachment < ActiveRecord::Base
21 21 belongs_to :container, :polymorphic => true
22 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 25 validates_length_of :filename, :maximum => 255
26 26 validates_length_of :disk_filename, :maximum => 255
27 27 validate :validate_max_file_size
28 28
29 29 acts_as_event :title => :filename,
30 30 :url => Proc.new {|o| {:controller => 'attachments', :action => 'download', :id => o.id, :filename => o.filename}}
31 31
32 32 acts_as_activity_provider :type => 'files',
33 33 :permission => :view_files,
34 34 :author_key => :author_id,
35 35 :find_options => {:select => "#{Attachment.table_name}.*",
36 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 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 39 acts_as_activity_provider :type => 'documents',
40 40 :permission => :view_documents,
41 41 :author_key => :author_id,
42 42 :find_options => {:select => "#{Attachment.table_name}.*",
43 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 44 "LEFT JOIN #{Project.table_name} ON #{Document.table_name}.project_id = #{Project.table_name}.id"}
45 45
46 46 cattr_accessor :storage_path
47 47 @@storage_path = Redmine::Configuration['attachments_storage_path'] || "#{Rails.root}/files"
48 48
49 49 before_save :files_to_final_location
50 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 61 # Returns an unsaved copy of the attachment
53 62 def copy(attributes=nil)
54 63 copy = self.class.new
55 64 copy.attributes = self.attributes.dup.except("id", "downloads")
56 65 copy.attributes = attributes if attributes
57 66 copy
58 67 end
59 68
60 69 def validate_max_file_size
61 70 if @temp_file && self.filesize > Setting.attachment_max_size.to_i.kilobytes
62 71 errors.add(:base, :too_long, :count => Setting.attachment_max_size.to_i.kilobytes)
63 72 end
64 73 end
65 74
66 75 def file=(incoming_file)
67 76 unless incoming_file.nil?
68 77 @temp_file = incoming_file
69 78 if @temp_file.size > 0
70 79 self.filename = sanitize_filename(@temp_file.original_filename)
71 80 self.disk_filename = Attachment.disk_filename(filename)
72 81 self.content_type = @temp_file.content_type.to_s.chomp
73 82 if content_type.blank?
74 83 self.content_type = Redmine::MimeType.of(filename)
75 84 end
76 85 self.filesize = @temp_file.size
77 86 end
78 87 end
79 88 end
80 89
81 90 def file
82 91 nil
83 92 end
84 93
85 94 # Copies the temporary file to its final location
86 95 # and computes its MD5 hash
87 96 def files_to_final_location
88 97 if @temp_file && (@temp_file.size > 0)
89 98 logger.info("Saving attachment '#{self.diskfile}' (#{@temp_file.size} bytes)")
90 99 md5 = Digest::MD5.new
91 100 File.open(diskfile, "wb") do |f|
92 101 buffer = ""
93 102 while (buffer = @temp_file.read(8192))
94 103 f.write(buffer)
95 104 md5.update(buffer)
96 105 end
97 106 end
98 107 self.digest = md5.hexdigest
99 108 end
100 109 @temp_file = nil
101 110 # Don't save the content type if it's longer than the authorized length
102 111 if self.content_type && self.content_type.length > 255
103 112 self.content_type = nil
104 113 end
105 114 end
106 115
107 116 # Deletes the file from the file system if it's not referenced by other attachments
108 117 def delete_from_disk
109 118 if Attachment.first(:conditions => ["disk_filename = ? AND id <> ?", disk_filename, id]).nil?
110 119 delete_from_disk!
111 120 end
112 121 end
113 122
114 123 # Returns file's location on disk
115 124 def diskfile
116 125 "#{@@storage_path}/#{self.disk_filename}"
117 126 end
118 127
119 128 def increment_download
120 129 increment!(:downloads)
121 130 end
122 131
123 132 def project
124 container.project
133 container.try(:project)
125 134 end
126 135
127 136 def visible?(user=User.current)
128 container.attachments_visible?(user)
137 container && container.attachments_visible?(user)
129 138 end
130 139
131 140 def deletable?(user=User.current)
132 container.attachments_deletable?(user)
141 container && container.attachments_deletable?(user)
133 142 end
134 143
135 144 def image?
136 145 self.filename =~ /\.(bmp|gif|jpg|jpe|jpeg|png)$/i
137 146 end
138 147
139 148 def is_text?
140 149 Redmine::MimeType.is_type?('text', filename)
141 150 end
142 151
143 152 def is_diff?
144 153 self.filename =~ /\.(patch|diff)$/i
145 154 end
146 155
147 156 # Returns true if the file is readable
148 157 def readable?
149 158 File.readable?(diskfile)
150 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 177 # Bulk attaches a set of files to an object
153 178 #
154 179 # Returns a Hash of the results:
155 180 # :files => array of the attached files
156 181 # :unsaved => array of the files that could not be attached
157 182 def self.attach_files(obj, attachments)
158 attached = []
159 if attachments && attachments.is_a?(Hash)
160 attachments.each_value do |attachment|
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}
183 result = obj.save_attachments(attachments, User.current)
184 obj.attach_saved_attachments
185 result
178 186 end
179 187
180 188 def self.latest_attach(attachments, filename)
181 189 attachments.sort_by(&:created_on).reverse.detect {
182 190 |att| att.filename.downcase == filename.downcase
183 191 }
184 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 199 private
187 200
188 201 # Physically deletes the file from the file system
189 202 def delete_from_disk!
190 203 if disk_filename.present? && File.exist?(diskfile)
191 204 File.delete(diskfile)
192 205 end
193 206 end
194 207
195 208 def sanitize_filename(value)
196 209 # get only the filename, not the whole path
197 210 just_filename = value.gsub(/^.*(\\|\/)/, '')
198 211
199 212 # Finally, replace invalid characters with underscore
200 213 @filename = just_filename.gsub(/[\/\?\%\*\:\|\"\'<>]+/, '_')
201 214 end
202 215
203 216 # Returns an ASCII or hashed filename
204 217 def self.disk_filename(filename)
205 218 timestamp = DateTime.now.strftime("%y%m%d%H%M%S")
206 219 ascii = ''
207 220 if filename =~ %r{^[a-zA-Z0-9_\.\-]*$}
208 221 ascii = filename
209 222 else
210 223 ascii = Digest::MD5.hexdigest(filename)
211 224 # keep the extension if any
212 225 ascii << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$}
213 226 end
214 227 while File.exist?(File.join(@@storage_path, "#{timestamp}_#{ascii}"))
215 228 timestamp.succ!
216 229 end
217 230 "#{timestamp}_#{ascii}"
218 231 end
219 232 end
@@ -1,1087 +1,1078
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class Issue < ActiveRecord::Base
19 19 include Redmine::SafeAttributes
20 20
21 21 belongs_to :project
22 22 belongs_to :tracker
23 23 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
24 24 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
25 25 belongs_to :assigned_to, :class_name => 'Principal', :foreign_key => 'assigned_to_id'
26 26 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
27 27 belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
28 28 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
29 29
30 30 has_many :journals, :as => :journalized, :dependent => :destroy
31 31 has_many :time_entries, :dependent => :delete_all
32 32 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
33 33
34 34 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
35 35 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
36 36
37 37 acts_as_nested_set :scope => 'root_id', :dependent => :destroy
38 38 acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed
39 39 acts_as_customizable
40 40 acts_as_watchable
41 41 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
42 42 :include => [:project, :journals],
43 43 # sort by id so that limited eager loading doesn't break with postgresql
44 44 :order_column => "#{table_name}.id"
45 45 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
46 46 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
47 47 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
48 48
49 49 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
50 50 :author_key => :author_id
51 51
52 52 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
53 53
54 54 attr_reader :current_journal
55 55
56 56 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
57 57
58 58 validates_length_of :subject, :maximum => 255
59 59 validates_inclusion_of :done_ratio, :in => 0..100
60 60 validates_numericality_of :estimated_hours, :allow_nil => true
61 61 validate :validate_issue
62 62
63 63 named_scope :visible, lambda {|*args| { :include => :project,
64 64 :conditions => Issue.visible_condition(args.shift || User.current, *args) } }
65 65
66 66 named_scope :open, lambda {|*args|
67 67 is_closed = args.size > 0 ? !args.first : false
68 68 {:conditions => ["#{IssueStatus.table_name}.is_closed = ?", is_closed], :include => :status}
69 69 }
70 70
71 71 named_scope :recently_updated, :order => "#{Issue.table_name}.updated_on DESC"
72 72 named_scope :with_limit, lambda { |limit| { :limit => limit} }
73 73 named_scope :on_active_project, :include => [:status, :project, :tracker],
74 74 :conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"]
75 75
76 76 before_create :default_assign
77 77 before_save :close_duplicates, :update_done_ratio_from_issue_status
78 78 after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?}
79 79 after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal
80 80 after_destroy :update_parent_attributes
81 81
82 82 # Returns a SQL conditions string used to find all issues visible by the specified user
83 83 def self.visible_condition(user, options={})
84 84 Project.allowed_to_condition(user, :view_issues, options) do |role, user|
85 85 case role.issues_visibility
86 86 when 'all'
87 87 nil
88 88 when 'default'
89 89 user_ids = [user.id] + user.groups.map(&:id)
90 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 91 when 'own'
92 92 user_ids = [user.id] + user.groups.map(&:id)
93 93 "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
94 94 else
95 95 '1=0'
96 96 end
97 97 end
98 98 end
99 99
100 100 # Returns true if usr or current user is allowed to view the issue
101 101 def visible?(usr=nil)
102 102 (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
103 103 case role.issues_visibility
104 104 when 'all'
105 105 true
106 106 when 'default'
107 107 !self.is_private? || self.author == user || user.is_or_belongs_to?(assigned_to)
108 108 when 'own'
109 109 self.author == user || user.is_or_belongs_to?(assigned_to)
110 110 else
111 111 false
112 112 end
113 113 end
114 114 end
115 115
116 116 def initialize(attributes=nil, *args)
117 117 super
118 118 if new_record?
119 119 # set default values for new records only
120 120 self.status ||= IssueStatus.default
121 121 self.priority ||= IssuePriority.default
122 122 self.watcher_user_ids = []
123 123 end
124 124 end
125 125
126 126 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
127 127 def available_custom_fields
128 128 (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields.all) : []
129 129 end
130 130
131 131 # Copies attributes from another issue, arg can be an id or an Issue
132 132 def copy_from(arg, options={})
133 133 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
134 134 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
135 135 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
136 136 self.status = issue.status
137 137 self.author = User.current
138 138 unless options[:attachments] == false
139 139 self.attachments = issue.attachments.map do |attachement|
140 140 attachement.copy(:container => self)
141 141 end
142 142 end
143 143 @copied_from = issue
144 144 self
145 145 end
146 146
147 147 # Returns an unsaved copy of the issue
148 148 def copy(attributes=nil)
149 149 copy = self.class.new.copy_from(self)
150 150 copy.attributes = attributes if attributes
151 151 copy
152 152 end
153 153
154 154 # Returns true if the issue is a copy
155 155 def copy?
156 156 @copied_from.present?
157 157 end
158 158
159 159 # Moves/copies an issue to a new project and tracker
160 160 # Returns the moved/copied issue on success, false on failure
161 161 def move_to_project(new_project, new_tracker=nil, options={})
162 162 ActiveSupport::Deprecation.warn "Issue#move_to_project is deprecated, use #project= instead."
163 163
164 164 if options[:copy]
165 165 issue = self.copy
166 166 else
167 167 issue = self
168 168 end
169 169
170 170 issue.init_journal(User.current, options[:notes])
171 171
172 172 # Preserve previous behaviour
173 173 # #move_to_project doesn't change tracker automatically
174 174 issue.send :project=, new_project, true
175 175 if new_tracker
176 176 issue.tracker = new_tracker
177 177 end
178 178 # Allow bulk setting of attributes on the issue
179 179 if options[:attributes]
180 180 issue.attributes = options[:attributes]
181 181 end
182 182
183 183 issue.save ? issue : false
184 184 end
185 185
186 186 def status_id=(sid)
187 187 self.status = nil
188 188 write_attribute(:status_id, sid)
189 189 end
190 190
191 191 def priority_id=(pid)
192 192 self.priority = nil
193 193 write_attribute(:priority_id, pid)
194 194 end
195 195
196 196 def category_id=(cid)
197 197 self.category = nil
198 198 write_attribute(:category_id, cid)
199 199 end
200 200
201 201 def fixed_version_id=(vid)
202 202 self.fixed_version = nil
203 203 write_attribute(:fixed_version_id, vid)
204 204 end
205 205
206 206 def tracker_id=(tid)
207 207 self.tracker = nil
208 208 result = write_attribute(:tracker_id, tid)
209 209 @custom_field_values = nil
210 210 result
211 211 end
212 212
213 213 def project_id=(project_id)
214 214 if project_id.to_s != self.project_id.to_s
215 215 self.project = (project_id.present? ? Project.find_by_id(project_id) : nil)
216 216 end
217 217 end
218 218
219 219 def project=(project, keep_tracker=false)
220 220 project_was = self.project
221 221 write_attribute(:project_id, project ? project.id : nil)
222 222 association_instance_set('project', project)
223 223 if project_was && project && project_was != project
224 224 unless keep_tracker || project.trackers.include?(tracker)
225 225 self.tracker = project.trackers.first
226 226 end
227 227 # Reassign to the category with same name if any
228 228 if category
229 229 self.category = project.issue_categories.find_by_name(category.name)
230 230 end
231 231 # Keep the fixed_version if it's still valid in the new_project
232 232 if fixed_version && fixed_version.project != project && !project.shared_versions.include?(fixed_version)
233 233 self.fixed_version = nil
234 234 end
235 235 if parent && parent.project_id != project_id
236 236 self.parent_issue_id = nil
237 237 end
238 238 @custom_field_values = nil
239 239 end
240 240 end
241 241
242 242 def description=(arg)
243 243 if arg.is_a?(String)
244 244 arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n")
245 245 end
246 246 write_attribute(:description, arg)
247 247 end
248 248
249 249 # Overrides attributes= so that project and tracker get assigned first
250 250 def attributes_with_project_and_tracker_first=(new_attributes, *args)
251 251 return if new_attributes.nil?
252 252 attrs = new_attributes.dup
253 253 attrs.stringify_keys!
254 254
255 255 %w(project project_id tracker tracker_id).each do |attr|
256 256 if attrs.has_key?(attr)
257 257 send "#{attr}=", attrs.delete(attr)
258 258 end
259 259 end
260 260 send :attributes_without_project_and_tracker_first=, attrs, *args
261 261 end
262 262 # Do not redefine alias chain on reload (see #4838)
263 263 alias_method_chain(:attributes=, :project_and_tracker_first) unless method_defined?(:attributes_without_project_and_tracker_first=)
264 264
265 265 def estimated_hours=(h)
266 266 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
267 267 end
268 268
269 269 safe_attributes 'project_id',
270 270 :if => lambda {|issue, user|
271 271 if issue.new_record?
272 272 issue.copy?
273 273 elsif user.allowed_to?(:move_issues, issue.project)
274 274 projects = Issue.allowed_target_projects_on_move(user)
275 275 projects.include?(issue.project) && projects.size > 1
276 276 end
277 277 }
278 278
279 279 safe_attributes 'tracker_id',
280 280 'status_id',
281 281 'category_id',
282 282 'assigned_to_id',
283 283 'priority_id',
284 284 'fixed_version_id',
285 285 'subject',
286 286 'description',
287 287 'start_date',
288 288 'due_date',
289 289 'done_ratio',
290 290 'estimated_hours',
291 291 'custom_field_values',
292 292 'custom_fields',
293 293 'lock_version',
294 294 :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
295 295
296 296 safe_attributes 'status_id',
297 297 'assigned_to_id',
298 298 'fixed_version_id',
299 299 'done_ratio',
300 300 'lock_version',
301 301 :if => lambda {|issue, user| issue.new_statuses_allowed_to(user).any? }
302 302
303 303 safe_attributes 'watcher_user_ids',
304 304 :if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)}
305 305
306 306 safe_attributes 'is_private',
307 307 :if => lambda {|issue, user|
308 308 user.allowed_to?(:set_issues_private, issue.project) ||
309 309 (issue.author == user && user.allowed_to?(:set_own_issues_private, issue.project))
310 310 }
311 311
312 312 safe_attributes 'parent_issue_id',
313 313 :if => lambda {|issue, user| (issue.new_record? || user.allowed_to?(:edit_issues, issue.project)) &&
314 314 user.allowed_to?(:manage_subtasks, issue.project)}
315 315
316 316 # Safely sets attributes
317 317 # Should be called from controllers instead of #attributes=
318 318 # attr_accessible is too rough because we still want things like
319 319 # Issue.new(:project => foo) to work
320 320 def safe_attributes=(attrs, user=User.current)
321 321 return unless attrs.is_a?(Hash)
322 322
323 323 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
324 324 attrs = delete_unsafe_attributes(attrs, user)
325 325 return if attrs.empty?
326 326
327 327 # Project and Tracker must be set before since new_statuses_allowed_to depends on it.
328 328 if p = attrs.delete('project_id')
329 329 if allowed_target_projects(user).collect(&:id).include?(p.to_i)
330 330 self.project_id = p
331 331 end
332 332 end
333 333
334 334 if t = attrs.delete('tracker_id')
335 335 self.tracker_id = t
336 336 end
337 337
338 338 if attrs['status_id']
339 339 unless new_statuses_allowed_to(user).collect(&:id).include?(attrs['status_id'].to_i)
340 340 attrs.delete('status_id')
341 341 end
342 342 end
343 343
344 344 unless leaf?
345 345 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
346 346 end
347 347
348 348 if attrs['parent_issue_id'].present?
349 349 attrs.delete('parent_issue_id') unless Issue.visible(user).exists?(attrs['parent_issue_id'].to_i)
350 350 end
351 351
352 352 # mass-assignment security bypass
353 353 self.send :attributes=, attrs, false
354 354 end
355 355
356 356 def done_ratio
357 357 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
358 358 status.default_done_ratio
359 359 else
360 360 read_attribute(:done_ratio)
361 361 end
362 362 end
363 363
364 364 def self.use_status_for_done_ratio?
365 365 Setting.issue_done_ratio == 'issue_status'
366 366 end
367 367
368 368 def self.use_field_for_done_ratio?
369 369 Setting.issue_done_ratio == 'issue_field'
370 370 end
371 371
372 372 def validate_issue
373 373 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
374 374 errors.add :due_date, :not_a_date
375 375 end
376 376
377 377 if self.due_date and self.start_date and self.due_date < self.start_date
378 378 errors.add :due_date, :greater_than_start_date
379 379 end
380 380
381 381 if start_date && soonest_start && start_date < soonest_start
382 382 errors.add :start_date, :invalid
383 383 end
384 384
385 385 if fixed_version
386 386 if !assignable_versions.include?(fixed_version)
387 387 errors.add :fixed_version_id, :inclusion
388 388 elsif reopened? && fixed_version.closed?
389 389 errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version)
390 390 end
391 391 end
392 392
393 393 # Checks that the issue can not be added/moved to a disabled tracker
394 394 if project && (tracker_id_changed? || project_id_changed?)
395 395 unless project.trackers.include?(tracker)
396 396 errors.add :tracker_id, :inclusion
397 397 end
398 398 end
399 399
400 400 # Checks parent issue assignment
401 401 if @parent_issue
402 402 if @parent_issue.project_id != project_id
403 403 errors.add :parent_issue_id, :not_same_project
404 404 elsif !new_record?
405 405 # moving an existing issue
406 406 if @parent_issue.root_id != root_id
407 407 # we can always move to another tree
408 408 elsif move_possible?(@parent_issue)
409 409 # move accepted inside tree
410 410 else
411 411 errors.add :parent_issue_id, :not_a_valid_parent
412 412 end
413 413 end
414 414 end
415 415 end
416 416
417 417 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
418 418 # even if the user turns off the setting later
419 419 def update_done_ratio_from_issue_status
420 420 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
421 421 self.done_ratio = status.default_done_ratio
422 422 end
423 423 end
424 424
425 425 def init_journal(user, notes = "")
426 426 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
427 427 if new_record?
428 428 @current_journal.notify = false
429 429 else
430 430 @attributes_before_change = attributes.dup
431 431 @custom_values_before_change = {}
432 432 self.custom_field_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
433 433 end
434 434 # Make sure updated_on is updated when adding a note.
435 435 updated_on_will_change!
436 436 @current_journal
437 437 end
438 438
439 439 # Returns the id of the last journal or nil
440 440 def last_journal_id
441 441 if new_record?
442 442 nil
443 443 else
444 444 journals.first(:order => "#{Journal.table_name}.id DESC").try(:id)
445 445 end
446 446 end
447 447
448 448 # Return true if the issue is closed, otherwise false
449 449 def closed?
450 450 self.status.is_closed?
451 451 end
452 452
453 453 # Return true if the issue is being reopened
454 454 def reopened?
455 455 if !new_record? && status_id_changed?
456 456 status_was = IssueStatus.find_by_id(status_id_was)
457 457 status_new = IssueStatus.find_by_id(status_id)
458 458 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
459 459 return true
460 460 end
461 461 end
462 462 false
463 463 end
464 464
465 465 # Return true if the issue is being closed
466 466 def closing?
467 467 if !new_record? && status_id_changed?
468 468 status_was = IssueStatus.find_by_id(status_id_was)
469 469 status_new = IssueStatus.find_by_id(status_id)
470 470 if status_was && status_new && !status_was.is_closed? && status_new.is_closed?
471 471 return true
472 472 end
473 473 end
474 474 false
475 475 end
476 476
477 477 # Returns true if the issue is overdue
478 478 def overdue?
479 479 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
480 480 end
481 481
482 482 # Is the amount of work done less than it should for the due date
483 483 def behind_schedule?
484 484 return false if start_date.nil? || due_date.nil?
485 485 done_date = start_date + ((due_date - start_date+1)* done_ratio/100).floor
486 486 return done_date <= Date.today
487 487 end
488 488
489 489 # Does this issue have children?
490 490 def children?
491 491 !leaf?
492 492 end
493 493
494 494 # Users the issue can be assigned to
495 495 def assignable_users
496 496 users = project.assignable_users
497 497 users << author if author
498 498 users << assigned_to if assigned_to
499 499 users.uniq.sort
500 500 end
501 501
502 502 # Versions that the issue can be assigned to
503 503 def assignable_versions
504 504 @assignable_versions ||= (project.shared_versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort
505 505 end
506 506
507 507 # Returns true if this issue is blocked by another issue that is still open
508 508 def blocked?
509 509 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
510 510 end
511 511
512 512 # Returns an array of status that user is able to apply
513 513 def new_statuses_allowed_to(user=User.current, include_default=false)
514 514 statuses = status.find_new_statuses_allowed_to(
515 515 user.admin ? Role.all : user.roles_for_project(project),
516 516 tracker,
517 517 author == user,
518 518 assigned_to_id_changed? ? assigned_to_id_was == user.id : assigned_to_id == user.id
519 519 )
520 520 statuses << status unless statuses.empty?
521 521 statuses << IssueStatus.default if include_default
522 522 statuses = statuses.uniq.sort
523 523 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
524 524 end
525 525
526 526 def assigned_to_was
527 527 if assigned_to_id_changed? && assigned_to_id_was.present?
528 528 @assigned_to_was ||= User.find_by_id(assigned_to_id_was)
529 529 end
530 530 end
531 531
532 532 # Returns the mail adresses of users that should be notified
533 533 def recipients
534 534 notified = []
535 535 # Author and assignee are always notified unless they have been
536 536 # locked or don't want to be notified
537 537 notified << author if author
538 538 if assigned_to
539 539 notified += (assigned_to.is_a?(Group) ? assigned_to.users : [assigned_to])
540 540 end
541 541 if assigned_to_was
542 542 notified += (assigned_to_was.is_a?(Group) ? assigned_to_was.users : [assigned_to_was])
543 543 end
544 544 notified = notified.select {|u| u.active? && u.notify_about?(self)}
545 545
546 546 notified += project.notified_users
547 547 notified.uniq!
548 548 # Remove users that can not view the issue
549 549 notified.reject! {|user| !visible?(user)}
550 550 notified.collect(&:mail)
551 551 end
552 552
553 553 # Returns the number of hours spent on this issue
554 554 def spent_hours
555 555 @spent_hours ||= time_entries.sum(:hours) || 0
556 556 end
557 557
558 558 # Returns the total number of hours spent on this issue and its descendants
559 559 #
560 560 # Example:
561 561 # spent_hours => 0.0
562 562 # spent_hours => 50.2
563 563 def total_spent_hours
564 564 @total_spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours",
565 565 :joins => "LEFT JOIN #{TimeEntry.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id").to_f || 0.0
566 566 end
567 567
568 568 def relations
569 569 @relations ||= (relations_from + relations_to).sort
570 570 end
571 571
572 572 # Preloads relations for a collection of issues
573 573 def self.load_relations(issues)
574 574 if issues.any?
575 575 relations = IssueRelation.all(:conditions => ["issue_from_id IN (:ids) OR issue_to_id IN (:ids)", {:ids => issues.map(&:id)}])
576 576 issues.each do |issue|
577 577 issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
578 578 end
579 579 end
580 580 end
581 581
582 582 # Preloads visible spent time for a collection of issues
583 583 def self.load_visible_spent_hours(issues, user=User.current)
584 584 if issues.any?
585 585 hours_by_issue_id = TimeEntry.visible(user).sum(:hours, :group => :issue_id)
586 586 issues.each do |issue|
587 587 issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0)
588 588 end
589 589 end
590 590 end
591 591
592 592 # Finds an issue relation given its id.
593 593 def find_relation(relation_id)
594 594 IssueRelation.find(relation_id, :conditions => ["issue_to_id = ? OR issue_from_id = ?", id, id])
595 595 end
596 596
597 597 def all_dependent_issues(except=[])
598 598 except << self
599 599 dependencies = []
600 600 relations_from.each do |relation|
601 601 if relation.issue_to && !except.include?(relation.issue_to)
602 602 dependencies << relation.issue_to
603 603 dependencies += relation.issue_to.all_dependent_issues(except)
604 604 end
605 605 end
606 606 dependencies
607 607 end
608 608
609 609 # Returns an array of issues that duplicate this one
610 610 def duplicates
611 611 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
612 612 end
613 613
614 614 # Returns the due date or the target due date if any
615 615 # Used on gantt chart
616 616 def due_before
617 617 due_date || (fixed_version ? fixed_version.effective_date : nil)
618 618 end
619 619
620 620 # Returns the time scheduled for this issue.
621 621 #
622 622 # Example:
623 623 # Start Date: 2/26/09, End Date: 3/04/09
624 624 # duration => 6
625 625 def duration
626 626 (start_date && due_date) ? due_date - start_date : 0
627 627 end
628 628
629 629 def soonest_start
630 630 @soonest_start ||= (
631 631 relations_to.collect{|relation| relation.successor_soonest_start} +
632 632 ancestors.collect(&:soonest_start)
633 633 ).compact.max
634 634 end
635 635
636 636 def reschedule_after(date)
637 637 return if date.nil?
638 638 if leaf?
639 639 if start_date.nil? || start_date < date
640 640 self.start_date, self.due_date = date, date + duration
641 641 begin
642 642 save
643 643 rescue ActiveRecord::StaleObjectError
644 644 reload
645 645 self.start_date, self.due_date = date, date + duration
646 646 save
647 647 end
648 648 end
649 649 else
650 650 leaves.each do |leaf|
651 651 leaf.reschedule_after(date)
652 652 end
653 653 end
654 654 end
655 655
656 656 def <=>(issue)
657 657 if issue.nil?
658 658 -1
659 659 elsif root_id != issue.root_id
660 660 (root_id || 0) <=> (issue.root_id || 0)
661 661 else
662 662 (lft || 0) <=> (issue.lft || 0)
663 663 end
664 664 end
665 665
666 666 def to_s
667 667 "#{tracker} ##{id}: #{subject}"
668 668 end
669 669
670 670 # Returns a string of css classes that apply to the issue
671 671 def css_classes
672 672 s = "issue status-#{status.position} priority-#{priority.position}"
673 673 s << ' closed' if closed?
674 674 s << ' overdue' if overdue?
675 675 s << ' child' if child?
676 676 s << ' parent' unless leaf?
677 677 s << ' private' if is_private?
678 678 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
679 679 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
680 680 s
681 681 end
682 682
683 # Saves an issue, time_entry, attachments, and a journal from the parameters
684 # Returns false if save fails
683 # Saves an issue and a time_entry from the parameters
685 684 def save_issue_with_child_records(params, existing_time_entry=nil)
686 685 Issue.transaction do
687 686 if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, project)
688 687 @time_entry = existing_time_entry || TimeEntry.new
689 688 @time_entry.project = project
690 689 @time_entry.issue = self
691 690 @time_entry.user = User.current
692 691 @time_entry.spent_on = User.current.today
693 692 @time_entry.attributes = params[:time_entry]
694 693 self.time_entries << @time_entry
695 694 end
696 695
697 if valid?
698 attachments = Attachment.attach_files(self, params[:attachments])
696 # TODO: Rename hook
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 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})
701 begin
702 if save
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
700 Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
701 else
702 raise ActiveRecord::Rollback
712 703 end
713 704 end
714 705 end
715 706
716 707 # Unassigns issues from +version+ if it's no longer shared with issue's project
717 708 def self.update_versions_from_sharing_change(version)
718 709 # Update issues assigned to the version
719 710 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
720 711 end
721 712
722 713 # Unassigns issues from versions that are no longer shared
723 714 # after +project+ was moved
724 715 def self.update_versions_from_hierarchy_change(project)
725 716 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
726 717 # Update issues of the moved projects and issues assigned to a version of a moved project
727 718 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
728 719 end
729 720
730 721 def parent_issue_id=(arg)
731 722 parent_issue_id = arg.blank? ? nil : arg.to_i
732 723 if parent_issue_id && @parent_issue = Issue.find_by_id(parent_issue_id)
733 724 @parent_issue.id
734 725 else
735 726 @parent_issue = nil
736 727 nil
737 728 end
738 729 end
739 730
740 731 def parent_issue_id
741 732 if instance_variable_defined? :@parent_issue
742 733 @parent_issue.nil? ? nil : @parent_issue.id
743 734 else
744 735 parent_id
745 736 end
746 737 end
747 738
748 739 # Extracted from the ReportsController.
749 740 def self.by_tracker(project)
750 741 count_and_group_by(:project => project,
751 742 :field => 'tracker_id',
752 743 :joins => Tracker.table_name)
753 744 end
754 745
755 746 def self.by_version(project)
756 747 count_and_group_by(:project => project,
757 748 :field => 'fixed_version_id',
758 749 :joins => Version.table_name)
759 750 end
760 751
761 752 def self.by_priority(project)
762 753 count_and_group_by(:project => project,
763 754 :field => 'priority_id',
764 755 :joins => IssuePriority.table_name)
765 756 end
766 757
767 758 def self.by_category(project)
768 759 count_and_group_by(:project => project,
769 760 :field => 'category_id',
770 761 :joins => IssueCategory.table_name)
771 762 end
772 763
773 764 def self.by_assigned_to(project)
774 765 count_and_group_by(:project => project,
775 766 :field => 'assigned_to_id',
776 767 :joins => User.table_name)
777 768 end
778 769
779 770 def self.by_author(project)
780 771 count_and_group_by(:project => project,
781 772 :field => 'author_id',
782 773 :joins => User.table_name)
783 774 end
784 775
785 776 def self.by_subproject(project)
786 777 ActiveRecord::Base.connection.select_all("select s.id as status_id,
787 778 s.is_closed as closed,
788 779 #{Issue.table_name}.project_id as project_id,
789 780 count(#{Issue.table_name}.id) as total
790 781 from
791 782 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s
792 783 where
793 784 #{Issue.table_name}.status_id=s.id
794 785 and #{Issue.table_name}.project_id = #{Project.table_name}.id
795 786 and #{visible_condition(User.current, :project => project, :with_subprojects => true)}
796 787 and #{Issue.table_name}.project_id <> #{project.id}
797 788 group by s.id, s.is_closed, #{Issue.table_name}.project_id") if project.descendants.active.any?
798 789 end
799 790 # End ReportsController extraction
800 791
801 792 # Returns an array of projects that user can assign the issue to
802 793 def allowed_target_projects(user=User.current)
803 794 if new_record?
804 795 Project.all(:conditions => Project.allowed_to_condition(user, :add_issues))
805 796 else
806 797 self.class.allowed_target_projects_on_move(user)
807 798 end
808 799 end
809 800
810 801 # Returns an array of projects that user can move issues to
811 802 def self.allowed_target_projects_on_move(user=User.current)
812 803 projects = []
813 804 if user.admin?
814 805 # admin is allowed to move issues to any active (visible) project
815 806 projects = Project.visible(user).all
816 807 elsif user.logged?
817 808 if Role.non_member.allowed_to?(:move_issues)
818 809 projects = Project.visible(user).all
819 810 else
820 811 user.memberships.each {|m| projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}}
821 812 end
822 813 end
823 814 projects
824 815 end
825 816
826 817 private
827 818
828 819 def after_project_change
829 820 # Update project_id on related time entries
830 821 TimeEntry.update_all(["project_id = ?", project_id], {:issue_id => id})
831 822
832 823 # Delete issue relations
833 824 unless Setting.cross_project_issue_relations?
834 825 relations_from.clear
835 826 relations_to.clear
836 827 end
837 828
838 829 # Move subtasks
839 830 children.each do |child|
840 831 # Change project and keep project
841 832 child.send :project=, project, true
842 833 unless child.save
843 834 raise ActiveRecord::Rollback
844 835 end
845 836 end
846 837 end
847 838
848 839 def update_nested_set_attributes
849 840 if root_id.nil?
850 841 # issue was just created
851 842 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
852 843 set_default_left_and_right
853 844 Issue.update_all("root_id = #{root_id}, lft = #{lft}, rgt = #{rgt}", ["id = ?", id])
854 845 if @parent_issue
855 846 move_to_child_of(@parent_issue)
856 847 end
857 848 reload
858 849 elsif parent_issue_id != parent_id
859 850 former_parent_id = parent_id
860 851 # moving an existing issue
861 852 if @parent_issue && @parent_issue.root_id == root_id
862 853 # inside the same tree
863 854 move_to_child_of(@parent_issue)
864 855 else
865 856 # to another tree
866 857 unless root?
867 858 move_to_right_of(root)
868 859 reload
869 860 end
870 861 old_root_id = root_id
871 862 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
872 863 target_maxright = nested_set_scope.maximum(right_column_name) || 0
873 864 offset = target_maxright + 1 - lft
874 865 Issue.update_all("root_id = #{root_id}, lft = lft + #{offset}, rgt = rgt + #{offset}",
875 866 ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
876 867 self[left_column_name] = lft + offset
877 868 self[right_column_name] = rgt + offset
878 869 if @parent_issue
879 870 move_to_child_of(@parent_issue)
880 871 end
881 872 end
882 873 reload
883 874 # delete invalid relations of all descendants
884 875 self_and_descendants.each do |issue|
885 876 issue.relations.each do |relation|
886 877 relation.destroy unless relation.valid?
887 878 end
888 879 end
889 880 # update former parent
890 881 recalculate_attributes_for(former_parent_id) if former_parent_id
891 882 end
892 883 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
893 884 end
894 885
895 886 def update_parent_attributes
896 887 recalculate_attributes_for(parent_id) if parent_id
897 888 end
898 889
899 890 def recalculate_attributes_for(issue_id)
900 891 if issue_id && p = Issue.find_by_id(issue_id)
901 892 # priority = highest priority of children
902 893 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :joins => :priority)
903 894 p.priority = IssuePriority.find_by_position(priority_position)
904 895 end
905 896
906 897 # start/due dates = lowest/highest dates of children
907 898 p.start_date = p.children.minimum(:start_date)
908 899 p.due_date = p.children.maximum(:due_date)
909 900 if p.start_date && p.due_date && p.due_date < p.start_date
910 901 p.start_date, p.due_date = p.due_date, p.start_date
911 902 end
912 903
913 904 # done ratio = weighted average ratio of leaves
914 905 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
915 906 leaves_count = p.leaves.count
916 907 if leaves_count > 0
917 908 average = p.leaves.average(:estimated_hours).to_f
918 909 if average == 0
919 910 average = 1
920 911 end
921 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 913 progress = done / (average * leaves_count)
923 914 p.done_ratio = progress.round
924 915 end
925 916 end
926 917
927 918 # estimate = sum of leaves estimates
928 919 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
929 920 p.estimated_hours = nil if p.estimated_hours == 0.0
930 921
931 922 # ancestors will be recursively updated
932 923 p.save(false)
933 924 end
934 925 end
935 926
936 927 # Update issues so their versions are not pointing to a
937 928 # fixed_version that is not shared with the issue's project
938 929 def self.update_versions(conditions=nil)
939 930 # Only need to update issues with a fixed_version from
940 931 # a different project and that is not systemwide shared
941 932 Issue.scoped(:conditions => conditions).all(
942 933 :conditions => "#{Issue.table_name}.fixed_version_id IS NOT NULL" +
943 934 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
944 935 " AND #{Version.table_name}.sharing <> 'system'",
945 936 :include => [:project, :fixed_version]
946 937 ).each do |issue|
947 938 next if issue.project.nil? || issue.fixed_version.nil?
948 939 unless issue.project.shared_versions.include?(issue.fixed_version)
949 940 issue.init_journal(User.current)
950 941 issue.fixed_version = nil
951 942 issue.save
952 943 end
953 944 end
954 945 end
955 946
956 947 # Callback on attachment deletion
957 948 def attachment_added(obj)
958 949 if @current_journal && !obj.new_record?
959 950 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :value => obj.filename)
960 951 end
961 952 end
962 953
963 954 # Callback on attachment deletion
964 955 def attachment_removed(obj)
965 956 journal = init_journal(User.current)
966 957 journal.details << JournalDetail.new(:property => 'attachment',
967 958 :prop_key => obj.id,
968 959 :old_value => obj.filename)
969 960 journal.save
970 961 end
971 962
972 963 # Default assignment based on category
973 964 def default_assign
974 965 if assigned_to.nil? && category && category.assigned_to
975 966 self.assigned_to = category.assigned_to
976 967 end
977 968 end
978 969
979 970 # Updates start/due dates of following issues
980 971 def reschedule_following_issues
981 972 if start_date_changed? || due_date_changed?
982 973 relations_from.each do |relation|
983 974 relation.set_issue_to_dates
984 975 end
985 976 end
986 977 end
987 978
988 979 # Closes duplicates if the issue is being closed
989 980 def close_duplicates
990 981 if closing?
991 982 duplicates.each do |duplicate|
992 983 # Reload is need in case the duplicate was updated by a previous duplicate
993 984 duplicate.reload
994 985 # Don't re-close it if it's already closed
995 986 next if duplicate.closed?
996 987 # Same user and notes
997 988 if @current_journal
998 989 duplicate.init_journal(@current_journal.user, @current_journal.notes)
999 990 end
1000 991 duplicate.update_attribute :status, self.status
1001 992 end
1002 993 end
1003 994 end
1004 995
1005 996 # Saves the changes in a Journal
1006 997 # Called after_save
1007 998 def create_journal
1008 999 if @current_journal
1009 1000 # attributes changes
1010 1001 if @attributes_before_change
1011 1002 (Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on)).each {|c|
1012 1003 before = @attributes_before_change[c]
1013 1004 after = send(c)
1014 1005 next if before == after || (before.blank? && after.blank?)
1015 1006 @current_journal.details << JournalDetail.new(:property => 'attr',
1016 1007 :prop_key => c,
1017 1008 :old_value => before,
1018 1009 :value => after)
1019 1010 }
1020 1011 end
1021 1012 if @custom_values_before_change
1022 1013 # custom fields changes
1023 1014 custom_field_values.each {|c|
1024 1015 before = @custom_values_before_change[c.custom_field_id]
1025 1016 after = c.value
1026 1017 next if before == after || (before.blank? && after.blank?)
1027 1018
1028 1019 if before.is_a?(Array) || after.is_a?(Array)
1029 1020 before = [before] unless before.is_a?(Array)
1030 1021 after = [after] unless after.is_a?(Array)
1031 1022
1032 1023 # values removed
1033 1024 (before - after).reject(&:blank?).each do |value|
1034 1025 @current_journal.details << JournalDetail.new(:property => 'cf',
1035 1026 :prop_key => c.custom_field_id,
1036 1027 :old_value => value,
1037 1028 :value => nil)
1038 1029 end
1039 1030 # values added
1040 1031 (after - before).reject(&:blank?).each do |value|
1041 1032 @current_journal.details << JournalDetail.new(:property => 'cf',
1042 1033 :prop_key => c.custom_field_id,
1043 1034 :old_value => nil,
1044 1035 :value => value)
1045 1036 end
1046 1037 else
1047 1038 @current_journal.details << JournalDetail.new(:property => 'cf',
1048 1039 :prop_key => c.custom_field_id,
1049 1040 :old_value => before,
1050 1041 :value => after)
1051 1042 end
1052 1043 }
1053 1044 end
1054 1045 @current_journal.save
1055 1046 # reset current journal
1056 1047 init_journal @current_journal.user, @current_journal.notes
1057 1048 end
1058 1049 end
1059 1050
1060 1051 # Query generator for selecting groups of issue counts for a project
1061 1052 # based on specific criteria
1062 1053 #
1063 1054 # Options
1064 1055 # * project - Project to search in.
1065 1056 # * field - String. Issue field to key off of in the grouping.
1066 1057 # * joins - String. The table name to join against.
1067 1058 def self.count_and_group_by(options)
1068 1059 project = options.delete(:project)
1069 1060 select_field = options.delete(:field)
1070 1061 joins = options.delete(:joins)
1071 1062
1072 1063 where = "#{Issue.table_name}.#{select_field}=j.id"
1073 1064
1074 1065 ActiveRecord::Base.connection.select_all("select s.id as status_id,
1075 1066 s.is_closed as closed,
1076 1067 j.id as #{select_field},
1077 1068 count(#{Issue.table_name}.id) as total
1078 1069 from
1079 1070 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s, #{joins} j
1080 1071 where
1081 1072 #{Issue.table_name}.status_id=s.id
1082 1073 and #{where}
1083 1074 and #{Issue.table_name}.project_id=#{Project.table_name}.id
1084 1075 and #{visible_condition(User.current, :project => project)}
1085 1076 group by s.id, s.is_closed, j.id")
1086 1077 end
1087 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 9 <span id="attachments_fields">
2 10 <span>
3 11 <%= file_field_tag 'attachments[1][file]', :size => 30, :id => nil, :class => 'file',
4 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 13 <%= text_field_tag 'attachments[1][description]', '', :id => nil, :class => 'description', :placeholder => l(:label_optional_description) %>
6 14 <%= link_to_function(image_tag('delete.png'), 'removeFileField(this)', :title => (l(:button_delete))) %>
7 15 </span>
8 16 </span>
9 17 <small><%= link_to l(:label_add_another_file), '#', :onclick => 'addFileField(); return false;' %>
10 18 (<%= l(:label_max_size) %>: <%= number_to_human_size(Setting.attachment_max_size.to_i.kilobytes) %>)
11 19 </small>
@@ -1,50 +1,50
1 1 <% labelled_form_for @issue, :html => {:id => 'issue-form', :multipart => true} do |f| %>
2 2 <%= error_messages_for 'issue', 'time_entry' %>
3 3 <%= render :partial => 'conflict' if @conflict %>
4 4 <div class="box">
5 5 <% if @edit_allowed || !@allowed_statuses.empty? %>
6 6 <fieldset class="tabular"><legend><%= l(:label_change_properties) %></legend>
7 7 <div id="all_attributes">
8 8 <%= render :partial => 'form', :locals => {:f => f} %>
9 9 </div>
10 10 </fieldset>
11 11 <% end %>
12 12 <% if User.current.allowed_to?(:log_time, @project) %>
13 13 <fieldset class="tabular"><legend><%= l(:button_log_time) %></legend>
14 14 <% labelled_fields_for :time_entry, @time_entry do |time_entry| %>
15 15 <div class="splitcontentleft">
16 16 <p><%= time_entry.text_field :hours, :size => 6, :label => :label_spent_time %> <%= l(:field_hours) %></p>
17 17 </div>
18 18 <div class="splitcontentright">
19 19 <p><%= time_entry.select :activity_id, activity_collection_for_select_options %></p>
20 20 </div>
21 21 <p><%= time_entry.text_field :comments, :size => 60 %></p>
22 22 <% @time_entry.custom_field_values.each do |value| %>
23 23 <p><%= custom_field_tag_with_label :time_entry, value %></p>
24 24 <% end %>
25 25 <% end %>
26 26 </fieldset>
27 27 <% end %>
28 28
29 29 <fieldset><legend><%= l(:field_notes) %></legend>
30 30 <%= text_area_tag 'notes', @notes, :cols => 60, :rows => 10, :class => 'wiki-edit' %>
31 31 <%= wikitoolbar_for 'notes' %>
32 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 35 </fieldset>
36 36 </div>
37 37
38 38 <%= f.hidden_field :lock_version %>
39 39 <%= hidden_field_tag 'last_journal_id', params[:last_journal_id] || @issue.last_journal_id %>
40 40 <%= submit_tag l(:button_submit) %>
41 41 <%= link_to_remote l(:label_preview),
42 42 { :url => preview_edit_issue_path(:project_id => @project, :id => @issue),
43 43 :method => 'post',
44 44 :update => 'preview',
45 45 :with => 'Form.serialize("issue-form")',
46 46 :complete => "Element.scrollTo('preview')"
47 47 }, :accesskey => accesskey(:preview) %>
48 48 <% end %>
49 49
50 50 <div id="preview" class="wiki"></div>
@@ -1,50 +1,50
1 1 <h2><%=l(:label_issue_new)%></h2>
2 2
3 3 <%= call_hook(:view_issues_new_top, {:issue => @issue}) %>
4 4
5 5 <% labelled_form_for @issue, :url => project_issues_path(@project),
6 6 :html => {:id => 'issue-form', :multipart => true} do |f| %>
7 7 <%= error_messages_for 'issue' %>
8 8 <%= hidden_field_tag 'copy_from', params[:copy_from] if params[:copy_from] %>
9 9 <div class="box tabular">
10 10 <div id="all_attributes">
11 11 <%= render :partial => 'issues/form', :locals => {:f => f} %>
12 12 </div>
13 13
14 14 <% if @copy_from && @copy_from.attachments.any? %>
15 15 <p>
16 16 <label for="copy_attachments"><%= l(:label_copy_attachments) %></label>
17 17 <%= check_box_tag 'copy_attachments', '1', @copy_attachments %>
18 18 </p>
19 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 23 <% if @issue.safe_attribute? 'watcher_user_ids' -%>
24 24 <p id="watchers_form"><label><%= l(:label_issue_watchers) %></label>
25 25 <% @issue.project.users.sort.each do |user| -%>
26 26 <label class="floating"><%= check_box_tag 'issue[watcher_user_ids][]', user.id, @issue.watched_by?(user), :id => nil %> <%=h user %></label>
27 27 <% end -%>
28 28 </p>
29 29 <% end %>
30 30 </div>
31 31
32 32 <%= submit_tag l(:button_create) %>
33 33 <%= submit_tag l(:button_create_and_continue), :name => 'continue' %>
34 34 <%= link_to_remote l(:label_preview),
35 35 { :url => preview_new_issue_path(:project_id => @project),
36 36 :method => 'post',
37 37 :update => 'preview',
38 38 :with => "Form.serialize('issue-form')",
39 39 :complete => "Element.scrollTo('preview')"
40 40 }, :accesskey => accesskey(:preview) %>
41 41
42 42 <%= javascript_tag "Form.Element.focus('issue_subject');" %>
43 43 <% end %>
44 44
45 45 <div id="preview" class="wiki"></div>
46 46
47 47 <% content_for :header_tags do %>
48 48 <%= stylesheet_link_tag 'scm' %>
49 49 <%= robot_exclusion_tag %>
50 50 <% end %>
@@ -1,299 +1,307
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 4 # Copyright (C) 2006-2011 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 require File.expand_path('../../test_helper', __FILE__)
21 21 require 'attachments_controller'
22 22
23 23 # Re-raise errors caught by the controller.
24 24 class AttachmentsController; def rescue_action(e) raise e end; end
25 25
26 26 class AttachmentsControllerTest < ActionController::TestCase
27 27 fixtures :users, :projects, :roles, :members, :member_roles,
28 28 :enabled_modules, :issues, :trackers, :attachments,
29 29 :versions, :wiki_pages, :wikis, :documents
30 30
31 31 def setup
32 32 @controller = AttachmentsController.new
33 33 @request = ActionController::TestRequest.new
34 34 @response = ActionController::TestResponse.new
35 35 User.current = nil
36 36 set_fixtures_attachments_directory
37 37 end
38 38
39 39 def teardown
40 40 set_tmp_attachments_directory
41 41 end
42 42
43 43 def test_show_diff
44 44 ['inline', 'sbs'].each do |dt|
45 45 # 060719210727_changeset_utf8.diff
46 46 get :show, :id => 14, :type => dt
47 47 assert_response :success
48 48 assert_template 'diff'
49 49 assert_equal 'text/html', @response.content_type
50 50 assert_tag 'th',
51 51 :attributes => {:class => /filename/},
52 52 :content => /issues_controller.rb\t\(rΓ©vision 1484\)/
53 53 assert_tag 'td',
54 54 :attributes => {:class => /line-code/},
55 55 :content => /Demande créée avec succès/
56 56 end
57 57 set_tmp_attachments_directory
58 58 end
59 59
60 60 def test_show_diff_replcace_cannot_convert_content
61 61 with_settings :repositories_encodings => 'UTF-8' do
62 62 ['inline', 'sbs'].each do |dt|
63 63 # 060719210727_changeset_iso8859-1.diff
64 64 get :show, :id => 5, :type => dt
65 65 assert_response :success
66 66 assert_template 'diff'
67 67 assert_equal 'text/html', @response.content_type
68 68 assert_tag 'th',
69 69 :attributes => {:class => "filename"},
70 70 :content => /issues_controller.rb\t\(r\?vision 1484\)/
71 71 assert_tag 'td',
72 72 :attributes => {:class => /line-code/},
73 73 :content => /Demande cr\?\?e avec succ\?s/
74 74 end
75 75 end
76 76 set_tmp_attachments_directory
77 77 end
78 78
79 79 def test_show_diff_latin_1
80 80 with_settings :repositories_encodings => 'UTF-8,ISO-8859-1' do
81 81 ['inline', 'sbs'].each do |dt|
82 82 # 060719210727_changeset_iso8859-1.diff
83 83 get :show, :id => 5, :type => dt
84 84 assert_response :success
85 85 assert_template 'diff'
86 86 assert_equal 'text/html', @response.content_type
87 87 assert_tag 'th',
88 88 :attributes => {:class => "filename"},
89 89 :content => /issues_controller.rb\t\(rΓ©vision 1484\)/
90 90 assert_tag 'td',
91 91 :attributes => {:class => /line-code/},
92 92 :content => /Demande créée avec succès/
93 93 end
94 94 end
95 95 set_tmp_attachments_directory
96 96 end
97 97
98 98 def test_save_diff_type
99 99 @request.session[:user_id] = 1 # admin
100 100 user = User.find(1)
101 101 get :show, :id => 5
102 102 assert_response :success
103 103 assert_template 'diff'
104 104 user.reload
105 105 assert_equal "inline", user.pref[:diff_type]
106 106 get :show, :id => 5, :type => 'sbs'
107 107 assert_response :success
108 108 assert_template 'diff'
109 109 user.reload
110 110 assert_equal "sbs", user.pref[:diff_type]
111 111 end
112 112
113 113 def test_show_text_file
114 114 get :show, :id => 4
115 115 assert_response :success
116 116 assert_template 'file'
117 117 assert_equal 'text/html', @response.content_type
118 118 set_tmp_attachments_directory
119 119 end
120 120
121 121 def test_show_text_file_utf_8
122 122 set_tmp_attachments_directory
123 123 a = Attachment.new(:container => Issue.find(1),
124 124 :file => uploaded_test_file("japanese-utf-8.txt", "text/plain"),
125 125 :author => User.find(1))
126 126 assert a.save
127 127 assert_equal 'japanese-utf-8.txt', a.filename
128 128
129 129 str_japanese = "\xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e"
130 130 str_japanese.force_encoding('UTF-8') if str_japanese.respond_to?(:force_encoding)
131 131
132 132 get :show, :id => a.id
133 133 assert_response :success
134 134 assert_template 'file'
135 135 assert_equal 'text/html', @response.content_type
136 136 assert_tag :tag => 'th',
137 137 :content => '1',
138 138 :attributes => { :class => 'line-num' },
139 139 :sibling => { :tag => 'td', :content => /#{str_japanese}/ }
140 140 end
141 141
142 142 def test_show_text_file_replcace_cannot_convert_content
143 143 set_tmp_attachments_directory
144 144 with_settings :repositories_encodings => 'UTF-8' do
145 145 a = Attachment.new(:container => Issue.find(1),
146 146 :file => uploaded_test_file("iso8859-1.txt", "text/plain"),
147 147 :author => User.find(1))
148 148 assert a.save
149 149 assert_equal 'iso8859-1.txt', a.filename
150 150
151 151 get :show, :id => a.id
152 152 assert_response :success
153 153 assert_template 'file'
154 154 assert_equal 'text/html', @response.content_type
155 155 assert_tag :tag => 'th',
156 156 :content => '7',
157 157 :attributes => { :class => 'line-num' },
158 158 :sibling => { :tag => 'td', :content => /Demande cr\?\?e avec succ\?s/ }
159 159 end
160 160 end
161 161
162 162 def test_show_text_file_latin_1
163 163 set_tmp_attachments_directory
164 164 with_settings :repositories_encodings => 'UTF-8,ISO-8859-1' do
165 165 a = Attachment.new(:container => Issue.find(1),
166 166 :file => uploaded_test_file("iso8859-1.txt", "text/plain"),
167 167 :author => User.find(1))
168 168 assert a.save
169 169 assert_equal 'iso8859-1.txt', a.filename
170 170
171 171 get :show, :id => a.id
172 172 assert_response :success
173 173 assert_template 'file'
174 174 assert_equal 'text/html', @response.content_type
175 175 assert_tag :tag => 'th',
176 176 :content => '7',
177 177 :attributes => { :class => 'line-num' },
178 178 :sibling => { :tag => 'td', :content => /Demande créée avec succès/ }
179 179 end
180 180 end
181 181
182 182 def test_show_text_file_should_send_if_too_big
183 183 Setting.file_max_size_displayed = 512
184 184 Attachment.find(4).update_attribute :filesize, 754.kilobyte
185 185
186 186 get :show, :id => 4
187 187 assert_response :success
188 188 assert_equal 'application/x-ruby', @response.content_type
189 189 set_tmp_attachments_directory
190 190 end
191 191
192 192 def test_show_other
193 193 get :show, :id => 6
194 194 assert_response :success
195 195 assert_equal 'application/octet-stream', @response.content_type
196 196 set_tmp_attachments_directory
197 197 end
198 198
199 199 def test_show_file_from_private_issue_without_permission
200 200 get :show, :id => 15
201 201 assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Fattachments%2F15'
202 202 set_tmp_attachments_directory
203 203 end
204 204
205 205 def test_show_file_from_private_issue_with_permission
206 206 @request.session[:user_id] = 2
207 207 get :show, :id => 15
208 208 assert_response :success
209 209 assert_tag 'h2', :content => /private.diff/
210 210 set_tmp_attachments_directory
211 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 221 def test_download_text_file
214 222 get :download, :id => 4
215 223 assert_response :success
216 224 assert_equal 'application/x-ruby', @response.content_type
217 225 set_tmp_attachments_directory
218 226 end
219 227
220 228 def test_download_version_file_with_issue_tracking_disabled
221 229 Project.find(1).disable_module! :issue_tracking
222 230 get :download, :id => 9
223 231 assert_response :success
224 232 end
225 233
226 234 def test_download_should_assign_content_type_if_blank
227 235 Attachment.find(4).update_attribute(:content_type, '')
228 236
229 237 get :download, :id => 4
230 238 assert_response :success
231 239 assert_equal 'text/x-ruby', @response.content_type
232 240 set_tmp_attachments_directory
233 241 end
234 242
235 243 def test_download_missing_file
236 244 get :download, :id => 2
237 245 assert_response 404
238 246 set_tmp_attachments_directory
239 247 end
240 248
241 249 def test_anonymous_on_private_private
242 250 get :download, :id => 7
243 251 assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Fattachments%2Fdownload%2F7'
244 252 set_tmp_attachments_directory
245 253 end
246 254
247 255 def test_destroy_issue_attachment
248 256 set_tmp_attachments_directory
249 257 issue = Issue.find(3)
250 258 @request.session[:user_id] = 2
251 259
252 260 assert_difference 'issue.attachments.count', -1 do
253 261 delete :destroy, :id => 1
254 262 end
255 263 # no referrer
256 264 assert_redirected_to '/projects/ecookbook'
257 265 assert_nil Attachment.find_by_id(1)
258 266 j = issue.journals.find(:first, :order => 'created_on DESC')
259 267 assert_equal 'attachment', j.details.first.property
260 268 assert_equal '1', j.details.first.prop_key
261 269 assert_equal 'error281.txt', j.details.first.old_value
262 270 end
263 271
264 272 def test_destroy_wiki_page_attachment
265 273 set_tmp_attachments_directory
266 274 @request.session[:user_id] = 2
267 275 assert_difference 'Attachment.count', -1 do
268 276 delete :destroy, :id => 3
269 277 assert_response 302
270 278 end
271 279 end
272 280
273 281 def test_destroy_project_attachment
274 282 set_tmp_attachments_directory
275 283 @request.session[:user_id] = 2
276 284 assert_difference 'Attachment.count', -1 do
277 285 delete :destroy, :id => 8
278 286 assert_response 302
279 287 end
280 288 end
281 289
282 290 def test_destroy_version_attachment
283 291 set_tmp_attachments_directory
284 292 @request.session[:user_id] = 2
285 293 assert_difference 'Attachment.count', -1 do
286 294 delete :destroy, :id => 9
287 295 assert_response 302
288 296 end
289 297 end
290 298
291 299 def test_destroy_without_permission
292 300 set_tmp_attachments_directory
293 301 assert_no_difference 'Attachment.count' do
294 302 delete :destroy, :id => 3
295 303 end
296 304 assert_response 302
297 305 assert Attachment.find_by_id(3)
298 306 end
299 307 end
@@ -1,2958 +1,3087
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../test_helper', __FILE__)
19 19 require 'issues_controller'
20 20
21 21 class IssuesControllerTest < ActionController::TestCase
22 22 fixtures :projects,
23 23 :users,
24 24 :roles,
25 25 :members,
26 26 :member_roles,
27 27 :issues,
28 28 :issue_statuses,
29 29 :versions,
30 30 :trackers,
31 31 :projects_trackers,
32 32 :issue_categories,
33 33 :enabled_modules,
34 34 :enumerations,
35 35 :attachments,
36 36 :workflows,
37 37 :custom_fields,
38 38 :custom_values,
39 39 :custom_fields_projects,
40 40 :custom_fields_trackers,
41 41 :time_entries,
42 42 :journals,
43 43 :journal_details,
44 44 :queries,
45 45 :repositories,
46 46 :changesets
47 47
48 48 include Redmine::I18n
49 49
50 50 def setup
51 51 @controller = IssuesController.new
52 52 @request = ActionController::TestRequest.new
53 53 @response = ActionController::TestResponse.new
54 54 User.current = nil
55 55 end
56 56
57 57 def test_index
58 58 with_settings :default_language => "en" do
59 59 get :index
60 60 assert_response :success
61 61 assert_template 'index'
62 62 assert_not_nil assigns(:issues)
63 63 assert_nil assigns(:project)
64 64 assert_tag :tag => 'a', :content => /Can't print recipes/
65 65 assert_tag :tag => 'a', :content => /Subproject issue/
66 66 # private projects hidden
67 67 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
68 68 assert_no_tag :tag => 'a', :content => /Issue on project 2/
69 69 # project column
70 70 assert_tag :tag => 'th', :content => /Project/
71 71 end
72 72 end
73 73
74 74 def test_index_should_not_list_issues_when_module_disabled
75 75 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
76 76 get :index
77 77 assert_response :success
78 78 assert_template 'index'
79 79 assert_not_nil assigns(:issues)
80 80 assert_nil assigns(:project)
81 81 assert_no_tag :tag => 'a', :content => /Can't print recipes/
82 82 assert_tag :tag => 'a', :content => /Subproject issue/
83 83 end
84 84
85 85 def test_index_should_list_visible_issues_only
86 86 get :index, :per_page => 100
87 87 assert_response :success
88 88 assert_not_nil assigns(:issues)
89 89 assert_nil assigns(:issues).detect {|issue| !issue.visible?}
90 90 end
91 91
92 92 def test_index_with_project
93 93 Setting.display_subprojects_issues = 0
94 94 get :index, :project_id => 1
95 95 assert_response :success
96 96 assert_template 'index'
97 97 assert_not_nil assigns(:issues)
98 98 assert_tag :tag => 'a', :content => /Can't print recipes/
99 99 assert_no_tag :tag => 'a', :content => /Subproject issue/
100 100 end
101 101
102 102 def test_index_with_project_and_subprojects
103 103 Setting.display_subprojects_issues = 1
104 104 get :index, :project_id => 1
105 105 assert_response :success
106 106 assert_template 'index'
107 107 assert_not_nil assigns(:issues)
108 108 assert_tag :tag => 'a', :content => /Can't print recipes/
109 109 assert_tag :tag => 'a', :content => /Subproject issue/
110 110 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
111 111 end
112 112
113 113 def test_index_with_project_and_subprojects_should_show_private_subprojects
114 114 @request.session[:user_id] = 2
115 115 Setting.display_subprojects_issues = 1
116 116 get :index, :project_id => 1
117 117 assert_response :success
118 118 assert_template 'index'
119 119 assert_not_nil assigns(:issues)
120 120 assert_tag :tag => 'a', :content => /Can't print recipes/
121 121 assert_tag :tag => 'a', :content => /Subproject issue/
122 122 assert_tag :tag => 'a', :content => /Issue of a private subproject/
123 123 end
124 124
125 125 def test_index_with_project_and_default_filter
126 126 get :index, :project_id => 1, :set_filter => 1
127 127 assert_response :success
128 128 assert_template 'index'
129 129 assert_not_nil assigns(:issues)
130 130
131 131 query = assigns(:query)
132 132 assert_not_nil query
133 133 # default filter
134 134 assert_equal({'status_id' => {:operator => 'o', :values => ['']}}, query.filters)
135 135 end
136 136
137 137 def test_index_with_project_and_filter
138 138 get :index, :project_id => 1, :set_filter => 1,
139 139 :f => ['tracker_id'],
140 140 :op => {'tracker_id' => '='},
141 141 :v => {'tracker_id' => ['1']}
142 142 assert_response :success
143 143 assert_template 'index'
144 144 assert_not_nil assigns(:issues)
145 145
146 146 query = assigns(:query)
147 147 assert_not_nil query
148 148 assert_equal({'tracker_id' => {:operator => '=', :values => ['1']}}, query.filters)
149 149 end
150 150
151 151 def test_index_with_short_filters
152 152
153 153 to_test = {
154 154 'status_id' => {
155 155 'o' => { :op => 'o', :values => [''] },
156 156 'c' => { :op => 'c', :values => [''] },
157 157 '7' => { :op => '=', :values => ['7'] },
158 158 '7|3|4' => { :op => '=', :values => ['7', '3', '4'] },
159 159 '=7' => { :op => '=', :values => ['7'] },
160 160 '!3' => { :op => '!', :values => ['3'] },
161 161 '!7|3|4' => { :op => '!', :values => ['7', '3', '4'] }},
162 162 'subject' => {
163 163 'This is a subject' => { :op => '=', :values => ['This is a subject'] },
164 164 'o' => { :op => '=', :values => ['o'] },
165 165 '~This is part of a subject' => { :op => '~', :values => ['This is part of a subject'] },
166 166 '!~This is part of a subject' => { :op => '!~', :values => ['This is part of a subject'] }},
167 167 'tracker_id' => {
168 168 '3' => { :op => '=', :values => ['3'] },
169 169 '=3' => { :op => '=', :values => ['3'] }},
170 170 'start_date' => {
171 171 '2011-10-12' => { :op => '=', :values => ['2011-10-12'] },
172 172 '=2011-10-12' => { :op => '=', :values => ['2011-10-12'] },
173 173 '>=2011-10-12' => { :op => '>=', :values => ['2011-10-12'] },
174 174 '<=2011-10-12' => { :op => '<=', :values => ['2011-10-12'] },
175 175 '><2011-10-01|2011-10-30' => { :op => '><', :values => ['2011-10-01', '2011-10-30'] },
176 176 '<t+2' => { :op => '<t+', :values => ['2'] },
177 177 '>t+2' => { :op => '>t+', :values => ['2'] },
178 178 't+2' => { :op => 't+', :values => ['2'] },
179 179 't' => { :op => 't', :values => [''] },
180 180 'w' => { :op => 'w', :values => [''] },
181 181 '>t-2' => { :op => '>t-', :values => ['2'] },
182 182 '<t-2' => { :op => '<t-', :values => ['2'] },
183 183 't-2' => { :op => 't-', :values => ['2'] }},
184 184 'created_on' => {
185 185 '>=2011-10-12' => { :op => '>=', :values => ['2011-10-12'] },
186 186 '<t+2' => { :op => '=', :values => ['<t+2'] },
187 187 '>t+2' => { :op => '=', :values => ['>t+2'] },
188 188 't+2' => { :op => 't', :values => ['+2'] }},
189 189 'cf_1' => {
190 190 'c' => { :op => '=', :values => ['c'] },
191 191 '!c' => { :op => '!', :values => ['c'] },
192 192 '!*' => { :op => '!*', :values => [''] },
193 193 '*' => { :op => '*', :values => [''] }},
194 194 'estimated_hours' => {
195 195 '=13.4' => { :op => '=', :values => ['13.4'] },
196 196 '>=45' => { :op => '>=', :values => ['45'] },
197 197 '<=125' => { :op => '<=', :values => ['125'] },
198 198 '><10.5|20.5' => { :op => '><', :values => ['10.5', '20.5'] },
199 199 '!*' => { :op => '!*', :values => [''] },
200 200 '*' => { :op => '*', :values => [''] }}
201 201 }
202 202
203 203 default_filter = { 'status_id' => {:operator => 'o', :values => [''] }}
204 204
205 205 to_test.each do |field, expression_and_expected|
206 206 expression_and_expected.each do |filter_expression, expected|
207 207
208 208 get :index, :set_filter => 1, field => filter_expression
209 209
210 210 assert_response :success
211 211 assert_template 'index'
212 212 assert_not_nil assigns(:issues)
213 213
214 214 query = assigns(:query)
215 215 assert_not_nil query
216 216 assert query.has_filter?(field)
217 217 assert_equal(default_filter.merge({field => {:operator => expected[:op], :values => expected[:values]}}), query.filters)
218 218 end
219 219 end
220 220
221 221 end
222 222
223 223 def test_index_with_project_and_empty_filters
224 224 get :index, :project_id => 1, :set_filter => 1, :fields => ['']
225 225 assert_response :success
226 226 assert_template 'index'
227 227 assert_not_nil assigns(:issues)
228 228
229 229 query = assigns(:query)
230 230 assert_not_nil query
231 231 # no filter
232 232 assert_equal({}, query.filters)
233 233 end
234 234
235 235 def test_index_with_query
236 236 get :index, :project_id => 1, :query_id => 5
237 237 assert_response :success
238 238 assert_template 'index'
239 239 assert_not_nil assigns(:issues)
240 240 assert_nil assigns(:issue_count_by_group)
241 241 end
242 242
243 243 def test_index_with_query_grouped_by_tracker
244 244 get :index, :project_id => 1, :query_id => 6
245 245 assert_response :success
246 246 assert_template 'index'
247 247 assert_not_nil assigns(:issues)
248 248 assert_not_nil assigns(:issue_count_by_group)
249 249 end
250 250
251 251 def test_index_with_query_grouped_by_list_custom_field
252 252 get :index, :project_id => 1, :query_id => 9
253 253 assert_response :success
254 254 assert_template 'index'
255 255 assert_not_nil assigns(:issues)
256 256 assert_not_nil assigns(:issue_count_by_group)
257 257 end
258 258
259 259 def test_index_with_query_id_and_project_id_should_set_session_query
260 260 get :index, :project_id => 1, :query_id => 4
261 261 assert_response :success
262 262 assert_kind_of Hash, session[:query]
263 263 assert_equal 4, session[:query][:id]
264 264 assert_equal 1, session[:query][:project_id]
265 265 end
266 266
267 267 def test_index_with_cross_project_query_in_session_should_show_project_issues
268 268 q = Query.create!(:name => "test", :user_id => 2, :is_public => false, :project => nil)
269 269 @request.session[:query] = {:id => q.id, :project_id => 1}
270 270
271 271 with_settings :display_subprojects_issues => '0' do
272 272 get :index, :project_id => 1
273 273 end
274 274 assert_response :success
275 275 assert_not_nil assigns(:query)
276 276 assert_equal q.id, assigns(:query).id
277 277 assert_equal 1, assigns(:query).project_id
278 278 assert_equal [1], assigns(:issues).map(&:project_id).uniq
279 279 end
280 280
281 281 def test_private_query_should_not_be_available_to_other_users
282 282 q = Query.create!(:name => "private", :user => User.find(2), :is_public => false, :project => nil)
283 283 @request.session[:user_id] = 3
284 284
285 285 get :index, :query_id => q.id
286 286 assert_response 403
287 287 end
288 288
289 289 def test_private_query_should_be_available_to_its_user
290 290 q = Query.create!(:name => "private", :user => User.find(2), :is_public => false, :project => nil)
291 291 @request.session[:user_id] = 2
292 292
293 293 get :index, :query_id => q.id
294 294 assert_response :success
295 295 end
296 296
297 297 def test_public_query_should_be_available_to_other_users
298 298 q = Query.create!(:name => "private", :user => User.find(2), :is_public => true, :project => nil)
299 299 @request.session[:user_id] = 3
300 300
301 301 get :index, :query_id => q.id
302 302 assert_response :success
303 303 end
304 304
305 305 def test_index_csv
306 306 get :index, :format => 'csv'
307 307 assert_response :success
308 308 assert_not_nil assigns(:issues)
309 309 assert_equal 'text/csv', @response.content_type
310 310 assert @response.body.starts_with?("#,")
311 311 lines = @response.body.chomp.split("\n")
312 312 assert_equal assigns(:query).columns.size + 1, lines[0].split(',').size
313 313 end
314 314
315 315 def test_index_csv_with_project
316 316 get :index, :project_id => 1, :format => 'csv'
317 317 assert_response :success
318 318 assert_not_nil assigns(:issues)
319 319 assert_equal 'text/csv', @response.content_type
320 320 end
321 321
322 322 def test_index_csv_with_description
323 323 get :index, :format => 'csv', :description => '1'
324 324 assert_response :success
325 325 assert_not_nil assigns(:issues)
326 326 assert_equal 'text/csv', @response.content_type
327 327 assert @response.body.starts_with?("#,")
328 328 lines = @response.body.chomp.split("\n")
329 329 assert_equal assigns(:query).columns.size + 2, lines[0].split(',').size
330 330 end
331 331
332 332 def test_index_csv_with_spent_time_column
333 333 issue = Issue.generate!(:project_id => 1, :tracker_id => 1, :subject => 'test_index_csv_with_spent_time_column')
334 334 TimeEntry.generate!(:project_id => issue.project_id, :issue_id => issue.id, :hours => 7.33)
335 335
336 336 get :index, :format => 'csv', :set_filter => '1', :c => %w(subject spent_hours)
337 337 assert_response :success
338 338 assert_equal 'text/csv', @response.content_type
339 339 lines = @response.body.chomp.split("\n")
340 340 assert_include "#{issue.id},#{issue.subject},7.33", lines
341 341 end
342 342
343 343 def test_index_csv_with_all_columns
344 344 get :index, :format => 'csv', :columns => 'all'
345 345 assert_response :success
346 346 assert_not_nil assigns(:issues)
347 347 assert_equal 'text/csv', @response.content_type
348 348 assert @response.body.starts_with?("#,")
349 349 lines = @response.body.chomp.split("\n")
350 350 assert_equal assigns(:query).available_columns.size + 1, lines[0].split(',').size
351 351 end
352 352
353 353 def test_index_csv_with_multi_column_field
354 354 CustomField.find(1).update_attribute :multiple, true
355 355 issue = Issue.find(1)
356 356 issue.custom_field_values = {1 => ['MySQL', 'Oracle']}
357 357 issue.save!
358 358
359 359 get :index, :format => 'csv', :columns => 'all'
360 360 assert_response :success
361 361 lines = @response.body.chomp.split("\n")
362 362 assert lines.detect {|line| line.include?('"MySQL, Oracle"')}
363 363 end
364 364
365 365 def test_index_csv_big_5
366 366 with_settings :default_language => "zh-TW" do
367 367 str_utf8 = "\xe4\xb8\x80\xe6\x9c\x88"
368 368 str_big5 = "\xa4@\xa4\xeb"
369 369 if str_utf8.respond_to?(:force_encoding)
370 370 str_utf8.force_encoding('UTF-8')
371 371 str_big5.force_encoding('Big5')
372 372 end
373 373 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
374 374 :status_id => 1, :priority => IssuePriority.all.first,
375 375 :subject => str_utf8)
376 376 assert issue.save
377 377
378 378 get :index, :project_id => 1,
379 379 :f => ['subject'],
380 380 :op => '=', :values => [str_utf8],
381 381 :format => 'csv'
382 382 assert_equal 'text/csv', @response.content_type
383 383 lines = @response.body.chomp.split("\n")
384 384 s1 = "\xaa\xac\xbaA"
385 385 if str_utf8.respond_to?(:force_encoding)
386 386 s1.force_encoding('Big5')
387 387 end
388 388 assert lines[0].include?(s1)
389 389 assert lines[1].include?(str_big5)
390 390 end
391 391 end
392 392
393 393 def test_index_csv_cannot_convert_should_be_replaced_big_5
394 394 with_settings :default_language => "zh-TW" do
395 395 str_utf8 = "\xe4\xbb\xa5\xe5\x86\x85"
396 396 if str_utf8.respond_to?(:force_encoding)
397 397 str_utf8.force_encoding('UTF-8')
398 398 end
399 399 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
400 400 :status_id => 1, :priority => IssuePriority.all.first,
401 401 :subject => str_utf8)
402 402 assert issue.save
403 403
404 404 get :index, :project_id => 1,
405 405 :f => ['subject'],
406 406 :op => '=', :values => [str_utf8],
407 407 :c => ['status', 'subject'],
408 408 :format => 'csv',
409 409 :set_filter => 1
410 410 assert_equal 'text/csv', @response.content_type
411 411 lines = @response.body.chomp.split("\n")
412 412 s1 = "\xaa\xac\xbaA" # status
413 413 if str_utf8.respond_to?(:force_encoding)
414 414 s1.force_encoding('Big5')
415 415 end
416 416 assert lines[0].include?(s1)
417 417 s2 = lines[1].split(",")[2]
418 418 if s1.respond_to?(:force_encoding)
419 419 s3 = "\xa5H?" # subject
420 420 s3.force_encoding('Big5')
421 421 assert_equal s3, s2
422 422 elsif RUBY_PLATFORM == 'java'
423 423 assert_equal "??", s2
424 424 else
425 425 assert_equal "\xa5H???", s2
426 426 end
427 427 end
428 428 end
429 429
430 430 def test_index_csv_tw
431 431 with_settings :default_language => "zh-TW" do
432 432 str1 = "test_index_csv_tw"
433 433 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
434 434 :status_id => 1, :priority => IssuePriority.all.first,
435 435 :subject => str1, :estimated_hours => '1234.5')
436 436 assert issue.save
437 437 assert_equal 1234.5, issue.estimated_hours
438 438
439 439 get :index, :project_id => 1,
440 440 :f => ['subject'],
441 441 :op => '=', :values => [str1],
442 442 :c => ['estimated_hours', 'subject'],
443 443 :format => 'csv',
444 444 :set_filter => 1
445 445 assert_equal 'text/csv', @response.content_type
446 446 lines = @response.body.chomp.split("\n")
447 447 assert_equal "#{issue.id},1234.50,#{str1}", lines[1]
448 448
449 449 str_tw = "Traditional Chinese (\xe7\xb9\x81\xe9\xab\x94\xe4\xb8\xad\xe6\x96\x87)"
450 450 if str_tw.respond_to?(:force_encoding)
451 451 str_tw.force_encoding('UTF-8')
452 452 end
453 453 assert_equal str_tw, l(:general_lang_name)
454 454 assert_equal ',', l(:general_csv_separator)
455 455 assert_equal '.', l(:general_csv_decimal_separator)
456 456 end
457 457 end
458 458
459 459 def test_index_csv_fr
460 460 with_settings :default_language => "fr" do
461 461 str1 = "test_index_csv_fr"
462 462 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
463 463 :status_id => 1, :priority => IssuePriority.all.first,
464 464 :subject => str1, :estimated_hours => '1234.5')
465 465 assert issue.save
466 466 assert_equal 1234.5, issue.estimated_hours
467 467
468 468 get :index, :project_id => 1,
469 469 :f => ['subject'],
470 470 :op => '=', :values => [str1],
471 471 :c => ['estimated_hours', 'subject'],
472 472 :format => 'csv',
473 473 :set_filter => 1
474 474 assert_equal 'text/csv', @response.content_type
475 475 lines = @response.body.chomp.split("\n")
476 476 assert_equal "#{issue.id};1234,50;#{str1}", lines[1]
477 477
478 478 str_fr = "Fran\xc3\xa7ais"
479 479 if str_fr.respond_to?(:force_encoding)
480 480 str_fr.force_encoding('UTF-8')
481 481 end
482 482 assert_equal str_fr, l(:general_lang_name)
483 483 assert_equal ';', l(:general_csv_separator)
484 484 assert_equal ',', l(:general_csv_decimal_separator)
485 485 end
486 486 end
487 487
488 488 def test_index_pdf
489 489 ["en", "zh", "zh-TW", "ja", "ko"].each do |lang|
490 490 with_settings :default_language => lang do
491 491
492 492 get :index
493 493 assert_response :success
494 494 assert_template 'index'
495 495
496 496 if lang == "ja"
497 497 if RUBY_PLATFORM != 'java'
498 498 assert_equal "CP932", l(:general_pdf_encoding)
499 499 end
500 500 if RUBY_PLATFORM == 'java' && l(:general_pdf_encoding) == "CP932"
501 501 next
502 502 end
503 503 end
504 504
505 505 get :index, :format => 'pdf'
506 506 assert_response :success
507 507 assert_not_nil assigns(:issues)
508 508 assert_equal 'application/pdf', @response.content_type
509 509
510 510 get :index, :project_id => 1, :format => 'pdf'
511 511 assert_response :success
512 512 assert_not_nil assigns(:issues)
513 513 assert_equal 'application/pdf', @response.content_type
514 514
515 515 get :index, :project_id => 1, :query_id => 6, :format => 'pdf'
516 516 assert_response :success
517 517 assert_not_nil assigns(:issues)
518 518 assert_equal 'application/pdf', @response.content_type
519 519 end
520 520 end
521 521 end
522 522
523 523 def test_index_pdf_with_query_grouped_by_list_custom_field
524 524 get :index, :project_id => 1, :query_id => 9, :format => 'pdf'
525 525 assert_response :success
526 526 assert_not_nil assigns(:issues)
527 527 assert_not_nil assigns(:issue_count_by_group)
528 528 assert_equal 'application/pdf', @response.content_type
529 529 end
530 530
531 531 def test_index_sort
532 532 get :index, :sort => 'tracker,id:desc'
533 533 assert_response :success
534 534
535 535 sort_params = @request.session['issues_index_sort']
536 536 assert sort_params.is_a?(String)
537 537 assert_equal 'tracker,id:desc', sort_params
538 538
539 539 issues = assigns(:issues)
540 540 assert_not_nil issues
541 541 assert !issues.empty?
542 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 543 end
544 544
545 545 def test_index_sort_by_field_not_included_in_columns
546 546 Setting.issue_list_default_columns = %w(subject author)
547 547 get :index, :sort => 'tracker'
548 548 end
549 549
550 550 def test_index_sort_by_assigned_to
551 551 get :index, :sort => 'assigned_to'
552 552 assert_response :success
553 553 assignees = assigns(:issues).collect(&:assigned_to).compact
554 554 assert_equal assignees.sort, assignees
555 555 end
556 556
557 557 def test_index_sort_by_assigned_to_desc
558 558 get :index, :sort => 'assigned_to:desc'
559 559 assert_response :success
560 560 assignees = assigns(:issues).collect(&:assigned_to).compact
561 561 assert_equal assignees.sort.reverse, assignees
562 562 end
563 563
564 564 def test_index_group_by_assigned_to
565 565 get :index, :group_by => 'assigned_to', :sort => 'priority'
566 566 assert_response :success
567 567 end
568 568
569 569 def test_index_sort_by_author
570 570 get :index, :sort => 'author'
571 571 assert_response :success
572 572 authors = assigns(:issues).collect(&:author)
573 573 assert_equal authors.sort, authors
574 574 end
575 575
576 576 def test_index_sort_by_author_desc
577 577 get :index, :sort => 'author:desc'
578 578 assert_response :success
579 579 authors = assigns(:issues).collect(&:author)
580 580 assert_equal authors.sort.reverse, authors
581 581 end
582 582
583 583 def test_index_group_by_author
584 584 get :index, :group_by => 'author', :sort => 'priority'
585 585 assert_response :success
586 586 end
587 587
588 588 def test_index_sort_by_spent_hours
589 589 get :index, :sort => 'spent_hours:desc'
590 590 assert_response :success
591 591 hours = assigns(:issues).collect(&:spent_hours)
592 592 assert_equal hours.sort.reverse, hours
593 593 end
594 594
595 595 def test_index_with_columns
596 596 columns = ['tracker', 'subject', 'assigned_to']
597 597 get :index, :set_filter => 1, :c => columns
598 598 assert_response :success
599 599
600 600 # query should use specified columns
601 601 query = assigns(:query)
602 602 assert_kind_of Query, query
603 603 assert_equal columns, query.column_names.map(&:to_s)
604 604
605 605 # columns should be stored in session
606 606 assert_kind_of Hash, session[:query]
607 607 assert_kind_of Array, session[:query][:column_names]
608 608 assert_equal columns, session[:query][:column_names].map(&:to_s)
609 609
610 610 # ensure only these columns are kept in the selected columns list
611 611 assert_tag :tag => 'select', :attributes => { :id => 'selected_columns' },
612 612 :children => { :count => 3 }
613 613 assert_no_tag :tag => 'option', :attributes => { :value => 'project' },
614 614 :parent => { :tag => 'select', :attributes => { :id => "selected_columns" } }
615 615 end
616 616
617 617 def test_index_without_project_should_implicitly_add_project_column_to_default_columns
618 618 Setting.issue_list_default_columns = ['tracker', 'subject', 'assigned_to']
619 619 get :index, :set_filter => 1
620 620
621 621 # query should use specified columns
622 622 query = assigns(:query)
623 623 assert_kind_of Query, query
624 624 assert_equal [:project, :tracker, :subject, :assigned_to], query.columns.map(&:name)
625 625 end
626 626
627 627 def test_index_without_project_and_explicit_default_columns_should_not_add_project_column
628 628 Setting.issue_list_default_columns = ['tracker', 'subject', 'assigned_to']
629 629 columns = ['tracker', 'subject', 'assigned_to']
630 630 get :index, :set_filter => 1, :c => columns
631 631
632 632 # query should use specified columns
633 633 query = assigns(:query)
634 634 assert_kind_of Query, query
635 635 assert_equal columns.map(&:to_sym), query.columns.map(&:name)
636 636 end
637 637
638 638 def test_index_with_custom_field_column
639 639 columns = %w(tracker subject cf_2)
640 640 get :index, :set_filter => 1, :c => columns
641 641 assert_response :success
642 642
643 643 # query should use specified columns
644 644 query = assigns(:query)
645 645 assert_kind_of Query, query
646 646 assert_equal columns, query.column_names.map(&:to_s)
647 647
648 648 assert_tag :td,
649 649 :attributes => {:class => 'cf_2 string'},
650 650 :ancestor => {:tag => 'table', :attributes => {:class => /issues/}}
651 651 end
652 652
653 653 def test_index_with_multi_custom_field_column
654 654 field = CustomField.find(1)
655 655 field.update_attribute :multiple, true
656 656 issue = Issue.find(1)
657 657 issue.custom_field_values = {1 => ['MySQL', 'Oracle']}
658 658 issue.save!
659 659
660 660 get :index, :set_filter => 1, :c => %w(tracker subject cf_1)
661 661 assert_response :success
662 662
663 663 assert_tag :td,
664 664 :attributes => {:class => /cf_1/},
665 665 :content => 'MySQL, Oracle'
666 666 end
667 667
668 668 def test_index_with_multi_user_custom_field_column
669 669 field = IssueCustomField.create!(:name => 'Multi user', :field_format => 'user', :multiple => true,
670 670 :tracker_ids => [1], :is_for_all => true)
671 671 issue = Issue.find(1)
672 672 issue.custom_field_values = {field.id => ['2', '3']}
673 673 issue.save!
674 674
675 675 get :index, :set_filter => 1, :c => ['tracker', 'subject', "cf_#{field.id}"]
676 676 assert_response :success
677 677
678 678 assert_tag :td,
679 679 :attributes => {:class => /cf_#{field.id}/},
680 680 :child => {:tag => 'a', :content => 'John Smith'}
681 681 end
682 682
683 683 def test_index_with_date_column
684 684 Issue.find(1).update_attribute :start_date, '1987-08-24'
685 685
686 686 with_settings :date_format => '%d/%m/%Y' do
687 687 get :index, :set_filter => 1, :c => %w(start_date)
688 688 assert_tag 'td', :attributes => {:class => /start_date/}, :content => '24/08/1987'
689 689 end
690 690 end
691 691
692 692 def test_index_with_done_ratio
693 693 Issue.find(1).update_attribute :done_ratio, 40
694 694
695 695 get :index, :set_filter => 1, :c => %w(done_ratio)
696 696 assert_tag 'td', :attributes => {:class => /done_ratio/},
697 697 :child => {:tag => 'table', :attributes => {:class => 'progress'},
698 698 :descendant => {:tag => 'td', :attributes => {:class => 'closed', :style => 'width: 40%;'}}
699 699 }
700 700 end
701 701
702 702 def test_index_with_spent_hours_column
703 703 get :index, :set_filter => 1, :c => %w(subject spent_hours)
704 704
705 705 assert_tag 'tr', :attributes => {:id => 'issue-3'},
706 706 :child => {
707 707 :tag => 'td', :attributes => {:class => /spent_hours/}, :content => '1.00'
708 708 }
709 709 end
710 710
711 711 def test_index_should_not_show_spent_hours_column_without_permission
712 712 Role.anonymous.remove_permission! :view_time_entries
713 713 get :index, :set_filter => 1, :c => %w(subject spent_hours)
714 714
715 715 assert_no_tag 'td', :attributes => {:class => /spent_hours/}
716 716 end
717 717
718 718 def test_index_with_fixed_version
719 719 get :index, :set_filter => 1, :c => %w(fixed_version)
720 720 assert_tag 'td', :attributes => {:class => /fixed_version/},
721 721 :child => {:tag => 'a', :content => '1.0', :attributes => {:href => '/versions/2'}}
722 722 end
723 723
724 724 def test_index_send_html_if_query_is_invalid
725 725 get :index, :f => ['start_date'], :op => {:start_date => '='}
726 726 assert_equal 'text/html', @response.content_type
727 727 assert_template 'index'
728 728 end
729 729
730 730 def test_index_send_nothing_if_query_is_invalid
731 731 get :index, :f => ['start_date'], :op => {:start_date => '='}, :format => 'csv'
732 732 assert_equal 'text/csv', @response.content_type
733 733 assert @response.body.blank?
734 734 end
735 735
736 736 def test_show_by_anonymous
737 737 get :show, :id => 1
738 738 assert_response :success
739 739 assert_template 'show'
740 740 assert_not_nil assigns(:issue)
741 741 assert_equal Issue.find(1), assigns(:issue)
742 742
743 743 # anonymous role is allowed to add a note
744 744 assert_tag :tag => 'form',
745 745 :descendant => { :tag => 'fieldset',
746 746 :child => { :tag => 'legend',
747 747 :content => /Notes/ } }
748 748 assert_tag :tag => 'title',
749 749 :content => "Bug #1: Can't print recipes - eCookbook - Redmine"
750 750 end
751 751
752 752 def test_show_by_manager
753 753 @request.session[:user_id] = 2
754 754 get :show, :id => 1
755 755 assert_response :success
756 756
757 757 assert_tag :tag => 'a',
758 758 :content => /Quote/
759 759
760 760 assert_tag :tag => 'form',
761 761 :descendant => { :tag => 'fieldset',
762 762 :child => { :tag => 'legend',
763 763 :content => /Change properties/ } },
764 764 :descendant => { :tag => 'fieldset',
765 765 :child => { :tag => 'legend',
766 766 :content => /Log time/ } },
767 767 :descendant => { :tag => 'fieldset',
768 768 :child => { :tag => 'legend',
769 769 :content => /Notes/ } }
770 770 end
771 771
772 772 def test_show_should_display_update_form
773 773 @request.session[:user_id] = 2
774 774 get :show, :id => 1
775 775 assert_response :success
776 776
777 777 assert_tag 'form', :attributes => {:id => 'issue-form'}
778 778 assert_tag 'input', :attributes => {:name => 'issue[is_private]'}
779 779 assert_tag 'select', :attributes => {:name => 'issue[project_id]'}
780 780 assert_tag 'select', :attributes => {:name => 'issue[tracker_id]'}
781 781 assert_tag 'input', :attributes => {:name => 'issue[subject]'}
782 782 assert_tag 'textarea', :attributes => {:name => 'issue[description]'}
783 783 assert_tag 'select', :attributes => {:name => 'issue[status_id]'}
784 784 assert_tag 'select', :attributes => {:name => 'issue[priority_id]'}
785 785 assert_tag 'select', :attributes => {:name => 'issue[assigned_to_id]'}
786 786 assert_tag 'select', :attributes => {:name => 'issue[category_id]'}
787 787 assert_tag 'select', :attributes => {:name => 'issue[fixed_version_id]'}
788 788 assert_tag 'input', :attributes => {:name => 'issue[parent_issue_id]'}
789 789 assert_tag 'input', :attributes => {:name => 'issue[start_date]'}
790 790 assert_tag 'input', :attributes => {:name => 'issue[due_date]'}
791 791 assert_tag 'select', :attributes => {:name => 'issue[done_ratio]'}
792 792 assert_tag 'input', :attributes => { :name => 'issue[custom_field_values][2]' }
793 793 assert_no_tag 'input', :attributes => {:name => 'issue[watcher_user_ids][]'}
794 794 assert_tag 'textarea', :attributes => {:name => 'notes'}
795 795 end
796 796
797 797 def test_show_should_display_update_form_with_minimal_permissions
798 798 Role.find(1).update_attribute :permissions, [:view_issues, :add_issue_notes]
799 799 Workflow.delete_all :role_id => 1
800 800
801 801 @request.session[:user_id] = 2
802 802 get :show, :id => 1
803 803 assert_response :success
804 804
805 805 assert_tag 'form', :attributes => {:id => 'issue-form'}
806 806 assert_no_tag 'input', :attributes => {:name => 'issue[is_private]'}
807 807 assert_no_tag 'select', :attributes => {:name => 'issue[project_id]'}
808 808 assert_no_tag 'select', :attributes => {:name => 'issue[tracker_id]'}
809 809 assert_no_tag 'input', :attributes => {:name => 'issue[subject]'}
810 810 assert_no_tag 'textarea', :attributes => {:name => 'issue[description]'}
811 811 assert_no_tag 'select', :attributes => {:name => 'issue[status_id]'}
812 812 assert_no_tag 'select', :attributes => {:name => 'issue[priority_id]'}
813 813 assert_no_tag 'select', :attributes => {:name => 'issue[assigned_to_id]'}
814 814 assert_no_tag 'select', :attributes => {:name => 'issue[category_id]'}
815 815 assert_no_tag 'select', :attributes => {:name => 'issue[fixed_version_id]'}
816 816 assert_no_tag 'input', :attributes => {:name => 'issue[parent_issue_id]'}
817 817 assert_no_tag 'input', :attributes => {:name => 'issue[start_date]'}
818 818 assert_no_tag 'input', :attributes => {:name => 'issue[due_date]'}
819 819 assert_no_tag 'select', :attributes => {:name => 'issue[done_ratio]'}
820 820 assert_no_tag 'input', :attributes => { :name => 'issue[custom_field_values][2]' }
821 821 assert_no_tag 'input', :attributes => {:name => 'issue[watcher_user_ids][]'}
822 822 assert_tag 'textarea', :attributes => {:name => 'notes'}
823 823 end
824 824
825 825 def test_show_should_display_update_form_with_workflow_permissions
826 826 Role.find(1).update_attribute :permissions, [:view_issues, :add_issue_notes]
827 827
828 828 @request.session[:user_id] = 2
829 829 get :show, :id => 1
830 830 assert_response :success
831 831
832 832 assert_tag 'form', :attributes => {:id => 'issue-form'}
833 833 assert_no_tag 'input', :attributes => {:name => 'issue[is_private]'}
834 834 assert_no_tag 'select', :attributes => {:name => 'issue[project_id]'}
835 835 assert_no_tag 'select', :attributes => {:name => 'issue[tracker_id]'}
836 836 assert_no_tag 'input', :attributes => {:name => 'issue[subject]'}
837 837 assert_no_tag 'textarea', :attributes => {:name => 'issue[description]'}
838 838 assert_tag 'select', :attributes => {:name => 'issue[status_id]'}
839 839 assert_no_tag 'select', :attributes => {:name => 'issue[priority_id]'}
840 840 assert_tag 'select', :attributes => {:name => 'issue[assigned_to_id]'}
841 841 assert_no_tag 'select', :attributes => {:name => 'issue[category_id]'}
842 842 assert_tag 'select', :attributes => {:name => 'issue[fixed_version_id]'}
843 843 assert_no_tag 'input', :attributes => {:name => 'issue[parent_issue_id]'}
844 844 assert_no_tag 'input', :attributes => {:name => 'issue[start_date]'}
845 845 assert_no_tag 'input', :attributes => {:name => 'issue[due_date]'}
846 846 assert_tag 'select', :attributes => {:name => 'issue[done_ratio]'}
847 847 assert_no_tag 'input', :attributes => { :name => 'issue[custom_field_values][2]' }
848 848 assert_no_tag 'input', :attributes => {:name => 'issue[watcher_user_ids][]'}
849 849 assert_tag 'textarea', :attributes => {:name => 'notes'}
850 850 end
851 851
852 852 def test_show_should_not_display_update_form_without_permissions
853 853 Role.find(1).update_attribute :permissions, [:view_issues]
854 854
855 855 @request.session[:user_id] = 2
856 856 get :show, :id => 1
857 857 assert_response :success
858 858
859 859 assert_no_tag 'form', :attributes => {:id => 'issue-form'}
860 860 end
861 861
862 862 def test_update_form_should_not_display_inactive_enumerations
863 863 @request.session[:user_id] = 2
864 864 get :show, :id => 1
865 865 assert_response :success
866 866
867 867 assert ! IssuePriority.find(15).active?
868 868 assert_no_tag :option, :attributes => {:value => '15'},
869 869 :parent => {:tag => 'select', :attributes => {:id => 'issue_priority_id'} }
870 870 end
871 871
872 872 def test_update_form_should_allow_attachment_upload
873 873 @request.session[:user_id] = 2
874 874 get :show, :id => 1
875 875
876 876 assert_tag :tag => 'form',
877 877 :attributes => {:id => 'issue-form', :method => 'post', :enctype => 'multipart/form-data'},
878 878 :descendant => {
879 879 :tag => 'input',
880 880 :attributes => {:type => 'file', :name => 'attachments[1][file]'}
881 881 }
882 882 end
883 883
884 884 def test_show_should_deny_anonymous_access_without_permission
885 885 Role.anonymous.remove_permission!(:view_issues)
886 886 get :show, :id => 1
887 887 assert_response :redirect
888 888 end
889 889
890 890 def test_show_should_deny_anonymous_access_to_private_issue
891 891 Issue.update_all(["is_private = ?", true], "id = 1")
892 892 get :show, :id => 1
893 893 assert_response :redirect
894 894 end
895 895
896 896 def test_show_should_deny_non_member_access_without_permission
897 897 Role.non_member.remove_permission!(:view_issues)
898 898 @request.session[:user_id] = 9
899 899 get :show, :id => 1
900 900 assert_response 403
901 901 end
902 902
903 903 def test_show_should_deny_non_member_access_to_private_issue
904 904 Issue.update_all(["is_private = ?", true], "id = 1")
905 905 @request.session[:user_id] = 9
906 906 get :show, :id => 1
907 907 assert_response 403
908 908 end
909 909
910 910 def test_show_should_deny_member_access_without_permission
911 911 Role.find(1).remove_permission!(:view_issues)
912 912 @request.session[:user_id] = 2
913 913 get :show, :id => 1
914 914 assert_response 403
915 915 end
916 916
917 917 def test_show_should_deny_member_access_to_private_issue_without_permission
918 918 Issue.update_all(["is_private = ?", true], "id = 1")
919 919 @request.session[:user_id] = 3
920 920 get :show, :id => 1
921 921 assert_response 403
922 922 end
923 923
924 924 def test_show_should_allow_author_access_to_private_issue
925 925 Issue.update_all(["is_private = ?, author_id = 3", true], "id = 1")
926 926 @request.session[:user_id] = 3
927 927 get :show, :id => 1
928 928 assert_response :success
929 929 end
930 930
931 931 def test_show_should_allow_assignee_access_to_private_issue
932 932 Issue.update_all(["is_private = ?, assigned_to_id = 3", true], "id = 1")
933 933 @request.session[:user_id] = 3
934 934 get :show, :id => 1
935 935 assert_response :success
936 936 end
937 937
938 938 def test_show_should_allow_member_access_to_private_issue_with_permission
939 939 Issue.update_all(["is_private = ?", true], "id = 1")
940 940 User.find(3).roles_for_project(Project.find(1)).first.update_attribute :issues_visibility, 'all'
941 941 @request.session[:user_id] = 3
942 942 get :show, :id => 1
943 943 assert_response :success
944 944 end
945 945
946 946 def test_show_should_not_disclose_relations_to_invisible_issues
947 947 Setting.cross_project_issue_relations = '1'
948 948 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(2), :relation_type => 'relates')
949 949 # Relation to a private project issue
950 950 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(4), :relation_type => 'relates')
951 951
952 952 get :show, :id => 1
953 953 assert_response :success
954 954
955 955 assert_tag :div, :attributes => { :id => 'relations' },
956 956 :descendant => { :tag => 'a', :content => /#2$/ }
957 957 assert_no_tag :div, :attributes => { :id => 'relations' },
958 958 :descendant => { :tag => 'a', :content => /#4$/ }
959 959 end
960 960
961 961 def test_show_should_list_subtasks
962 962 Issue.generate!(:project_id => 1, :author_id => 1, :tracker_id => 1, :parent_issue_id => 1, :subject => 'Child Issue')
963 963
964 964 get :show, :id => 1
965 965 assert_response :success
966 966 assert_tag 'div', :attributes => {:id => 'issue_tree'},
967 967 :descendant => {:tag => 'td', :content => /Child Issue/, :attributes => {:class => /subject/}}
968 968 end
969 969
970 970 def test_show_should_list_parents
971 971 issue = Issue.generate!(:project_id => 1, :author_id => 1, :tracker_id => 1, :parent_issue_id => 1, :subject => 'Child Issue')
972 972
973 973 get :show, :id => issue.id
974 974 assert_response :success
975 975 assert_tag 'div', :attributes => {:class => 'subject'},
976 976 :descendant => {:tag => 'h3', :content => 'Child Issue'}
977 977 assert_tag 'div', :attributes => {:class => 'subject'},
978 978 :descendant => {:tag => 'a', :attributes => {:href => '/issues/1'}}
979 979 end
980 980
981 981 def test_show_should_not_display_prev_next_links_without_query_in_session
982 982 get :show, :id => 1
983 983 assert_response :success
984 984 assert_nil assigns(:prev_issue_id)
985 985 assert_nil assigns(:next_issue_id)
986 986
987 987 assert_no_tag 'div', :attributes => {:class => /next-prev-links/}
988 988 end
989 989
990 990 def test_show_should_display_prev_next_links_with_query_in_session
991 991 @request.session[:query] = {:filters => {'status_id' => {:values => [''], :operator => 'o'}}, :project_id => nil}
992 992 @request.session['issues_index_sort'] = 'id'
993 993
994 994 with_settings :display_subprojects_issues => '0' do
995 995 get :show, :id => 3
996 996 end
997 997
998 998 assert_response :success
999 999 # Previous and next issues for all projects
1000 1000 assert_equal 2, assigns(:prev_issue_id)
1001 1001 assert_equal 5, assigns(:next_issue_id)
1002 1002
1003 1003 assert_tag 'div', :attributes => {:class => /next-prev-links/}
1004 1004 assert_tag 'a', :attributes => {:href => '/issues/2'}, :content => /Previous/
1005 1005 assert_tag 'a', :attributes => {:href => '/issues/5'}, :content => /Next/
1006 1006
1007 1007 count = Issue.open.visible.count
1008 1008 assert_tag 'span', :attributes => {:class => 'position'}, :content => "3 of #{count}"
1009 1009 end
1010 1010
1011 1011 def test_show_should_display_prev_next_links_with_saved_query_in_session
1012 1012 query = Query.create!(:name => 'test', :is_public => true, :user_id => 1,
1013 1013 :filters => {'status_id' => {:values => ['5'], :operator => '='}},
1014 1014 :sort_criteria => [['id', 'asc']])
1015 1015 @request.session[:query] = {:id => query.id, :project_id => nil}
1016 1016
1017 1017 get :show, :id => 11
1018 1018
1019 1019 assert_response :success
1020 1020 assert_equal query, assigns(:query)
1021 1021 # Previous and next issues for all projects
1022 1022 assert_equal 8, assigns(:prev_issue_id)
1023 1023 assert_equal 12, assigns(:next_issue_id)
1024 1024
1025 1025 assert_tag 'a', :attributes => {:href => '/issues/8'}, :content => /Previous/
1026 1026 assert_tag 'a', :attributes => {:href => '/issues/12'}, :content => /Next/
1027 1027 end
1028 1028
1029 1029 def test_show_should_display_prev_next_links_with_query_and_sort_on_association
1030 1030 @request.session[:query] = {:filters => {'status_id' => {:values => [''], :operator => 'o'}}, :project_id => nil}
1031 1031
1032 1032 %w(project tracker status priority author assigned_to category fixed_version).each do |assoc_sort|
1033 1033 @request.session['issues_index_sort'] = assoc_sort
1034 1034
1035 1035 get :show, :id => 3
1036 1036 assert_response :success, "Wrong response status for #{assoc_sort} sort"
1037 1037
1038 1038 assert_tag 'a', :content => /Previous/
1039 1039 assert_tag 'a', :content => /Next/
1040 1040 end
1041 1041 end
1042 1042
1043 1043 def test_show_should_display_prev_next_links_with_project_query_in_session
1044 1044 @request.session[:query] = {:filters => {'status_id' => {:values => [''], :operator => 'o'}}, :project_id => 1}
1045 1045 @request.session['issues_index_sort'] = 'id'
1046 1046
1047 1047 with_settings :display_subprojects_issues => '0' do
1048 1048 get :show, :id => 3
1049 1049 end
1050 1050
1051 1051 assert_response :success
1052 1052 # Previous and next issues inside project
1053 1053 assert_equal 2, assigns(:prev_issue_id)
1054 1054 assert_equal 7, assigns(:next_issue_id)
1055 1055
1056 1056 assert_tag 'a', :attributes => {:href => '/issues/2'}, :content => /Previous/
1057 1057 assert_tag 'a', :attributes => {:href => '/issues/7'}, :content => /Next/
1058 1058 end
1059 1059
1060 1060 def test_show_should_not_display_prev_link_for_first_issue
1061 1061 @request.session[:query] = {:filters => {'status_id' => {:values => [''], :operator => 'o'}}, :project_id => 1}
1062 1062 @request.session['issues_index_sort'] = 'id'
1063 1063
1064 1064 with_settings :display_subprojects_issues => '0' do
1065 1065 get :show, :id => 1
1066 1066 end
1067 1067
1068 1068 assert_response :success
1069 1069 assert_nil assigns(:prev_issue_id)
1070 1070 assert_equal 2, assigns(:next_issue_id)
1071 1071
1072 1072 assert_no_tag 'a', :content => /Previous/
1073 1073 assert_tag 'a', :attributes => {:href => '/issues/2'}, :content => /Next/
1074 1074 end
1075 1075
1076 1076 def test_show_should_not_display_prev_next_links_for_issue_not_in_query_results
1077 1077 @request.session[:query] = {:filters => {'status_id' => {:values => [''], :operator => 'c'}}, :project_id => 1}
1078 1078 @request.session['issues_index_sort'] = 'id'
1079 1079
1080 1080 get :show, :id => 1
1081 1081
1082 1082 assert_response :success
1083 1083 assert_nil assigns(:prev_issue_id)
1084 1084 assert_nil assigns(:next_issue_id)
1085 1085
1086 1086 assert_no_tag 'a', :content => /Previous/
1087 1087 assert_no_tag 'a', :content => /Next/
1088 1088 end
1089 1089
1090 1090 def test_show_should_display_visible_changesets_from_other_projects
1091 1091 project = Project.find(2)
1092 1092 issue = project.issues.first
1093 1093 issue.changeset_ids = [102]
1094 1094 issue.save!
1095 1095 project.disable_module! :repository
1096 1096
1097 1097 @request.session[:user_id] = 2
1098 1098 get :show, :id => issue.id
1099 1099 assert_tag 'a', :attributes => {:href => "/projects/ecookbook/repository/revisions/3"}
1100 1100 end
1101 1101
1102 1102 def test_show_with_multi_custom_field
1103 1103 field = CustomField.find(1)
1104 1104 field.update_attribute :multiple, true
1105 1105 issue = Issue.find(1)
1106 1106 issue.custom_field_values = {1 => ['MySQL', 'Oracle']}
1107 1107 issue.save!
1108 1108
1109 1109 get :show, :id => 1
1110 1110 assert_response :success
1111 1111
1112 1112 assert_tag :td, :content => 'MySQL, Oracle'
1113 1113 end
1114 1114
1115 1115 def test_show_with_multi_user_custom_field
1116 1116 field = IssueCustomField.create!(:name => 'Multi user', :field_format => 'user', :multiple => true,
1117 1117 :tracker_ids => [1], :is_for_all => true)
1118 1118 issue = Issue.find(1)
1119 1119 issue.custom_field_values = {field.id => ['2', '3']}
1120 1120 issue.save!
1121 1121
1122 1122 get :show, :id => 1
1123 1123 assert_response :success
1124 1124
1125 1125 # TODO: should display links
1126 1126 assert_tag :td, :content => 'Dave Lopper, John Smith'
1127 1127 end
1128 1128
1129 1129 def test_show_atom
1130 1130 get :show, :id => 2, :format => 'atom'
1131 1131 assert_response :success
1132 1132 assert_template 'journals/index'
1133 1133 # Inline image
1134 1134 assert_select 'content', :text => Regexp.new(Regexp.quote('http://test.host/attachments/download/10'))
1135 1135 end
1136 1136
1137 1137 def test_show_export_to_pdf
1138 1138 get :show, :id => 3, :format => 'pdf'
1139 1139 assert_response :success
1140 1140 assert_equal 'application/pdf', @response.content_type
1141 1141 assert @response.body.starts_with?('%PDF')
1142 1142 assert_not_nil assigns(:issue)
1143 1143 end
1144 1144
1145 1145 def test_get_new
1146 1146 @request.session[:user_id] = 2
1147 1147 get :new, :project_id => 1, :tracker_id => 1
1148 1148 assert_response :success
1149 1149 assert_template 'new'
1150 1150
1151 1151 assert_tag 'input', :attributes => {:name => 'issue[is_private]'}
1152 1152 assert_no_tag 'select', :attributes => {:name => 'issue[project_id]'}
1153 1153 assert_tag 'select', :attributes => {:name => 'issue[tracker_id]'}
1154 1154 assert_tag 'input', :attributes => {:name => 'issue[subject]'}
1155 1155 assert_tag 'textarea', :attributes => {:name => 'issue[description]'}
1156 1156 assert_tag 'select', :attributes => {:name => 'issue[status_id]'}
1157 1157 assert_tag 'select', :attributes => {:name => 'issue[priority_id]'}
1158 1158 assert_tag 'select', :attributes => {:name => 'issue[assigned_to_id]'}
1159 1159 assert_tag 'select', :attributes => {:name => 'issue[category_id]'}
1160 1160 assert_tag 'select', :attributes => {:name => 'issue[fixed_version_id]'}
1161 1161 assert_tag 'input', :attributes => {:name => 'issue[parent_issue_id]'}
1162 1162 assert_tag 'input', :attributes => {:name => 'issue[start_date]'}
1163 1163 assert_tag 'input', :attributes => {:name => 'issue[due_date]'}
1164 1164 assert_tag 'select', :attributes => {:name => 'issue[done_ratio]'}
1165 1165 assert_tag 'input', :attributes => { :name => 'issue[custom_field_values][2]', :value => 'Default string' }
1166 1166 assert_tag 'input', :attributes => {:name => 'issue[watcher_user_ids][]'}
1167 1167
1168 1168 # Be sure we don't display inactive IssuePriorities
1169 1169 assert ! IssuePriority.find(15).active?
1170 1170 assert_no_tag :option, :attributes => {:value => '15'},
1171 1171 :parent => {:tag => 'select', :attributes => {:id => 'issue_priority_id'} }
1172 1172 end
1173 1173
1174 1174 def test_get_new_with_minimal_permissions
1175 1175 Role.find(1).update_attribute :permissions, [:add_issues]
1176 1176 Workflow.delete_all :role_id => 1
1177 1177
1178 1178 @request.session[:user_id] = 2
1179 1179 get :new, :project_id => 1, :tracker_id => 1
1180 1180 assert_response :success
1181 1181 assert_template 'new'
1182 1182
1183 1183 assert_no_tag 'input', :attributes => {:name => 'issue[is_private]'}
1184 1184 assert_no_tag 'select', :attributes => {:name => 'issue[project_id]'}
1185 1185 assert_tag 'select', :attributes => {:name => 'issue[tracker_id]'}
1186 1186 assert_tag 'input', :attributes => {:name => 'issue[subject]'}
1187 1187 assert_tag 'textarea', :attributes => {:name => 'issue[description]'}
1188 1188 assert_tag 'select', :attributes => {:name => 'issue[status_id]'}
1189 1189 assert_tag 'select', :attributes => {:name => 'issue[priority_id]'}
1190 1190 assert_tag 'select', :attributes => {:name => 'issue[assigned_to_id]'}
1191 1191 assert_tag 'select', :attributes => {:name => 'issue[category_id]'}
1192 1192 assert_tag 'select', :attributes => {:name => 'issue[fixed_version_id]'}
1193 1193 assert_no_tag 'input', :attributes => {:name => 'issue[parent_issue_id]'}
1194 1194 assert_tag 'input', :attributes => {:name => 'issue[start_date]'}
1195 1195 assert_tag 'input', :attributes => {:name => 'issue[due_date]'}
1196 1196 assert_tag 'select', :attributes => {:name => 'issue[done_ratio]'}
1197 1197 assert_tag 'input', :attributes => { :name => 'issue[custom_field_values][2]', :value => 'Default string' }
1198 1198 assert_no_tag 'input', :attributes => {:name => 'issue[watcher_user_ids][]'}
1199 1199 end
1200 1200
1201 1201 def test_get_new_with_multi_custom_field
1202 1202 field = IssueCustomField.find(1)
1203 1203 field.update_attribute :multiple, true
1204 1204
1205 1205 @request.session[:user_id] = 2
1206 1206 get :new, :project_id => 1, :tracker_id => 1
1207 1207 assert_response :success
1208 1208 assert_template 'new'
1209 1209
1210 1210 assert_tag 'select',
1211 1211 :attributes => {:name => 'issue[custom_field_values][1][]', :multiple => 'multiple'},
1212 1212 :children => {:count => 3},
1213 1213 :child => {:tag => 'option', :attributes => {:value => 'MySQL'}, :content => 'MySQL'}
1214 1214 assert_tag 'input',
1215 1215 :attributes => {:name => 'issue[custom_field_values][1][]', :value => ''}
1216 1216 end
1217 1217
1218 1218 def test_get_new_with_multi_user_custom_field
1219 1219 field = IssueCustomField.create!(:name => 'Multi user', :field_format => 'user', :multiple => true,
1220 1220 :tracker_ids => [1], :is_for_all => true)
1221 1221
1222 1222 @request.session[:user_id] = 2
1223 1223 get :new, :project_id => 1, :tracker_id => 1
1224 1224 assert_response :success
1225 1225 assert_template 'new'
1226 1226
1227 1227 assert_tag 'select',
1228 1228 :attributes => {:name => "issue[custom_field_values][#{field.id}][]", :multiple => 'multiple'},
1229 1229 :children => {:count => Project.find(1).users.count},
1230 1230 :child => {:tag => 'option', :attributes => {:value => '2'}, :content => 'John Smith'}
1231 1231 assert_tag 'input',
1232 1232 :attributes => {:name => "issue[custom_field_values][#{field.id}][]", :value => ''}
1233 1233 end
1234 1234
1235 1235 def test_get_new_without_default_start_date_is_creation_date
1236 1236 Setting.default_issue_start_date_to_creation_date = 0
1237 1237
1238 1238 @request.session[:user_id] = 2
1239 1239 get :new, :project_id => 1, :tracker_id => 1
1240 1240 assert_response :success
1241 1241 assert_template 'new'
1242 1242
1243 1243 assert_tag :tag => 'input', :attributes => { :name => 'issue[start_date]',
1244 1244 :value => nil }
1245 1245 end
1246 1246
1247 1247 def test_get_new_with_default_start_date_is_creation_date
1248 1248 Setting.default_issue_start_date_to_creation_date = 1
1249 1249
1250 1250 @request.session[:user_id] = 2
1251 1251 get :new, :project_id => 1, :tracker_id => 1
1252 1252 assert_response :success
1253 1253 assert_template 'new'
1254 1254
1255 1255 assert_tag :tag => 'input', :attributes => { :name => 'issue[start_date]',
1256 1256 :value => Date.today.to_s }
1257 1257 end
1258 1258
1259 1259 def test_get_new_form_should_allow_attachment_upload
1260 1260 @request.session[:user_id] = 2
1261 1261 get :new, :project_id => 1, :tracker_id => 1
1262 1262
1263 1263 assert_tag :tag => 'form',
1264 1264 :attributes => {:id => 'issue-form', :method => 'post', :enctype => 'multipart/form-data'},
1265 1265 :descendant => {
1266 1266 :tag => 'input',
1267 1267 :attributes => {:type => 'file', :name => 'attachments[1][file]'}
1268 1268 }
1269 1269 end
1270 1270
1271 1271 def test_get_new_without_tracker_id
1272 1272 @request.session[:user_id] = 2
1273 1273 get :new, :project_id => 1
1274 1274 assert_response :success
1275 1275 assert_template 'new'
1276 1276
1277 1277 issue = assigns(:issue)
1278 1278 assert_not_nil issue
1279 1279 assert_equal Project.find(1).trackers.first, issue.tracker
1280 1280 end
1281 1281
1282 1282 def test_get_new_with_no_default_status_should_display_an_error
1283 1283 @request.session[:user_id] = 2
1284 1284 IssueStatus.delete_all
1285 1285
1286 1286 get :new, :project_id => 1
1287 1287 assert_response 500
1288 1288 assert_error_tag :content => /No default issue/
1289 1289 end
1290 1290
1291 1291 def test_get_new_with_no_tracker_should_display_an_error
1292 1292 @request.session[:user_id] = 2
1293 1293 Tracker.delete_all
1294 1294
1295 1295 get :new, :project_id => 1
1296 1296 assert_response 500
1297 1297 assert_error_tag :content => /No tracker/
1298 1298 end
1299 1299
1300 1300 def test_update_new_form
1301 1301 @request.session[:user_id] = 2
1302 1302 xhr :post, :new, :project_id => 1,
1303 1303 :issue => {:tracker_id => 2,
1304 1304 :subject => 'This is the test_new issue',
1305 1305 :description => 'This is the description',
1306 1306 :priority_id => 5}
1307 1307 assert_response :success
1308 1308 assert_template 'attributes'
1309 1309
1310 1310 issue = assigns(:issue)
1311 1311 assert_kind_of Issue, issue
1312 1312 assert_equal 1, issue.project_id
1313 1313 assert_equal 2, issue.tracker_id
1314 1314 assert_equal 'This is the test_new issue', issue.subject
1315 1315 end
1316 1316
1317 1317 def test_post_create
1318 1318 @request.session[:user_id] = 2
1319 1319 assert_difference 'Issue.count' do
1320 1320 post :create, :project_id => 1,
1321 1321 :issue => {:tracker_id => 3,
1322 1322 :status_id => 2,
1323 1323 :subject => 'This is the test_new issue',
1324 1324 :description => 'This is the description',
1325 1325 :priority_id => 5,
1326 1326 :start_date => '2010-11-07',
1327 1327 :estimated_hours => '',
1328 1328 :custom_field_values => {'2' => 'Value for field 2'}}
1329 1329 end
1330 1330 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
1331 1331
1332 1332 issue = Issue.find_by_subject('This is the test_new issue')
1333 1333 assert_not_nil issue
1334 1334 assert_equal 2, issue.author_id
1335 1335 assert_equal 3, issue.tracker_id
1336 1336 assert_equal 2, issue.status_id
1337 1337 assert_equal Date.parse('2010-11-07'), issue.start_date
1338 1338 assert_nil issue.estimated_hours
1339 1339 v = issue.custom_values.find(:first, :conditions => {:custom_field_id => 2})
1340 1340 assert_not_nil v
1341 1341 assert_equal 'Value for field 2', v.value
1342 1342 end
1343 1343
1344 1344 def test_post_new_with_group_assignment
1345 1345 group = Group.find(11)
1346 1346 project = Project.find(1)
1347 1347 project.members << Member.new(:principal => group, :roles => [Role.first])
1348 1348
1349 1349 with_settings :issue_group_assignment => '1' do
1350 1350 @request.session[:user_id] = 2
1351 1351 assert_difference 'Issue.count' do
1352 1352 post :create, :project_id => project.id,
1353 1353 :issue => {:tracker_id => 3,
1354 1354 :status_id => 1,
1355 1355 :subject => 'This is the test_new_with_group_assignment issue',
1356 1356 :assigned_to_id => group.id}
1357 1357 end
1358 1358 end
1359 1359 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
1360 1360
1361 1361 issue = Issue.find_by_subject('This is the test_new_with_group_assignment issue')
1362 1362 assert_not_nil issue
1363 1363 assert_equal group, issue.assigned_to
1364 1364 end
1365 1365
1366 1366 def test_post_create_without_start_date_and_default_start_date_is_not_creation_date
1367 1367 Setting.default_issue_start_date_to_creation_date = 0
1368 1368
1369 1369 @request.session[:user_id] = 2
1370 1370 assert_difference 'Issue.count' do
1371 1371 post :create, :project_id => 1,
1372 1372 :issue => {:tracker_id => 3,
1373 1373 :status_id => 2,
1374 1374 :subject => 'This is the test_new issue',
1375 1375 :description => 'This is the description',
1376 1376 :priority_id => 5,
1377 1377 :estimated_hours => '',
1378 1378 :custom_field_values => {'2' => 'Value for field 2'}}
1379 1379 end
1380 1380 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
1381 1381
1382 1382 issue = Issue.find_by_subject('This is the test_new issue')
1383 1383 assert_not_nil issue
1384 1384 assert_nil issue.start_date
1385 1385 end
1386 1386
1387 1387 def test_post_create_without_start_date_and_default_start_date_is_creation_date
1388 1388 Setting.default_issue_start_date_to_creation_date = 1
1389 1389
1390 1390 @request.session[:user_id] = 2
1391 1391 assert_difference 'Issue.count' do
1392 1392 post :create, :project_id => 1,
1393 1393 :issue => {:tracker_id => 3,
1394 1394 :status_id => 2,
1395 1395 :subject => 'This is the test_new issue',
1396 1396 :description => 'This is the description',
1397 1397 :priority_id => 5,
1398 1398 :estimated_hours => '',
1399 1399 :custom_field_values => {'2' => 'Value for field 2'}}
1400 1400 end
1401 1401 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
1402 1402
1403 1403 issue = Issue.find_by_subject('This is the test_new issue')
1404 1404 assert_not_nil issue
1405 1405 assert_equal Date.today, issue.start_date
1406 1406 end
1407 1407
1408 1408 def test_post_create_and_continue
1409 1409 @request.session[:user_id] = 2
1410 1410 assert_difference 'Issue.count' do
1411 1411 post :create, :project_id => 1,
1412 1412 :issue => {:tracker_id => 3, :subject => 'This is first issue', :priority_id => 5},
1413 1413 :continue => ''
1414 1414 end
1415 1415
1416 1416 issue = Issue.first(:order => 'id DESC')
1417 1417 assert_redirected_to :controller => 'issues', :action => 'new', :project_id => 'ecookbook', :issue => {:tracker_id => 3}
1418 1418 assert_not_nil flash[:notice], "flash was not set"
1419 1419 assert flash[:notice].include?("<a href='/issues/#{issue.id}'>##{issue.id}</a>"), "issue link not found in flash: #{flash[:notice]}"
1420 1420 end
1421 1421
1422 1422 def test_post_create_without_custom_fields_param
1423 1423 @request.session[:user_id] = 2
1424 1424 assert_difference 'Issue.count' do
1425 1425 post :create, :project_id => 1,
1426 1426 :issue => {:tracker_id => 1,
1427 1427 :subject => 'This is the test_new issue',
1428 1428 :description => 'This is the description',
1429 1429 :priority_id => 5}
1430 1430 end
1431 1431 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
1432 1432 end
1433 1433
1434 1434 def test_post_create_with_multi_custom_field
1435 1435 field = IssueCustomField.find_by_name('Database')
1436 1436 field.update_attribute(:multiple, true)
1437 1437
1438 1438 @request.session[:user_id] = 2
1439 1439 assert_difference 'Issue.count' do
1440 1440 post :create, :project_id => 1,
1441 1441 :issue => {:tracker_id => 1,
1442 1442 :subject => 'This is the test_new issue',
1443 1443 :description => 'This is the description',
1444 1444 :priority_id => 5,
1445 1445 :custom_field_values => {'1' => ['', 'MySQL', 'Oracle']}}
1446 1446 end
1447 1447 assert_response 302
1448 1448 issue = Issue.first(:order => 'id DESC')
1449 1449 assert_equal ['MySQL', 'Oracle'], issue.custom_field_value(1).sort
1450 1450 end
1451 1451
1452 1452 def test_post_create_with_empty_multi_custom_field
1453 1453 field = IssueCustomField.find_by_name('Database')
1454 1454 field.update_attribute(:multiple, true)
1455 1455
1456 1456 @request.session[:user_id] = 2
1457 1457 assert_difference 'Issue.count' do
1458 1458 post :create, :project_id => 1,
1459 1459 :issue => {:tracker_id => 1,
1460 1460 :subject => 'This is the test_new issue',
1461 1461 :description => 'This is the description',
1462 1462 :priority_id => 5,
1463 1463 :custom_field_values => {'1' => ['']}}
1464 1464 end
1465 1465 assert_response 302
1466 1466 issue = Issue.first(:order => 'id DESC')
1467 1467 assert_equal [''], issue.custom_field_value(1).sort
1468 1468 end
1469 1469
1470 1470 def test_post_create_with_multi_user_custom_field
1471 1471 field = IssueCustomField.create!(:name => 'Multi user', :field_format => 'user', :multiple => true,
1472 1472 :tracker_ids => [1], :is_for_all => true)
1473 1473
1474 1474 @request.session[:user_id] = 2
1475 1475 assert_difference 'Issue.count' do
1476 1476 post :create, :project_id => 1,
1477 1477 :issue => {:tracker_id => 1,
1478 1478 :subject => 'This is the test_new issue',
1479 1479 :description => 'This is the description',
1480 1480 :priority_id => 5,
1481 1481 :custom_field_values => {field.id.to_s => ['', '2', '3']}}
1482 1482 end
1483 1483 assert_response 302
1484 1484 issue = Issue.first(:order => 'id DESC')
1485 1485 assert_equal ['2', '3'], issue.custom_field_value(field).sort
1486 1486 end
1487 1487
1488 1488 def test_post_create_with_required_custom_field_and_without_custom_fields_param
1489 1489 field = IssueCustomField.find_by_name('Database')
1490 1490 field.update_attribute(:is_required, true)
1491 1491
1492 1492 @request.session[:user_id] = 2
1493 1493 assert_no_difference 'Issue.count' do
1494 1494 post :create, :project_id => 1,
1495 1495 :issue => {:tracker_id => 1,
1496 1496 :subject => 'This is the test_new issue',
1497 1497 :description => 'This is the description',
1498 1498 :priority_id => 5}
1499 1499 end
1500 1500 assert_response :success
1501 1501 assert_template 'new'
1502 1502 issue = assigns(:issue)
1503 1503 assert_not_nil issue
1504 1504 assert_error_tag :content => /Database can't be blank/
1505 1505 end
1506 1506
1507 1507 def test_post_create_with_watchers
1508 1508 @request.session[:user_id] = 2
1509 1509 ActionMailer::Base.deliveries.clear
1510 1510
1511 1511 assert_difference 'Watcher.count', 2 do
1512 1512 post :create, :project_id => 1,
1513 1513 :issue => {:tracker_id => 1,
1514 1514 :subject => 'This is a new issue with watchers',
1515 1515 :description => 'This is the description',
1516 1516 :priority_id => 5,
1517 1517 :watcher_user_ids => ['2', '3']}
1518 1518 end
1519 1519 issue = Issue.find_by_subject('This is a new issue with watchers')
1520 1520 assert_not_nil issue
1521 1521 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
1522 1522
1523 1523 # Watchers added
1524 1524 assert_equal [2, 3], issue.watcher_user_ids.sort
1525 1525 assert issue.watched_by?(User.find(3))
1526 1526 # Watchers notified
1527 1527 mail = ActionMailer::Base.deliveries.last
1528 1528 assert_kind_of TMail::Mail, mail
1529 1529 assert [mail.bcc, mail.cc].flatten.include?(User.find(3).mail)
1530 1530 end
1531 1531
1532 1532 def test_post_create_subissue
1533 1533 @request.session[:user_id] = 2
1534 1534
1535 1535 assert_difference 'Issue.count' do
1536 1536 post :create, :project_id => 1,
1537 1537 :issue => {:tracker_id => 1,
1538 1538 :subject => 'This is a child issue',
1539 1539 :parent_issue_id => 2}
1540 1540 end
1541 1541 issue = Issue.find_by_subject('This is a child issue')
1542 1542 assert_not_nil issue
1543 1543 assert_equal Issue.find(2), issue.parent
1544 1544 end
1545 1545
1546 1546 def test_post_create_subissue_with_non_numeric_parent_id
1547 1547 @request.session[:user_id] = 2
1548 1548
1549 1549 assert_difference 'Issue.count' do
1550 1550 post :create, :project_id => 1,
1551 1551 :issue => {:tracker_id => 1,
1552 1552 :subject => 'This is a child issue',
1553 1553 :parent_issue_id => 'ABC'}
1554 1554 end
1555 1555 issue = Issue.find_by_subject('This is a child issue')
1556 1556 assert_not_nil issue
1557 1557 assert_nil issue.parent
1558 1558 end
1559 1559
1560 1560 def test_post_create_private
1561 1561 @request.session[:user_id] = 2
1562 1562
1563 1563 assert_difference 'Issue.count' do
1564 1564 post :create, :project_id => 1,
1565 1565 :issue => {:tracker_id => 1,
1566 1566 :subject => 'This is a private issue',
1567 1567 :is_private => '1'}
1568 1568 end
1569 1569 issue = Issue.first(:order => 'id DESC')
1570 1570 assert issue.is_private?
1571 1571 end
1572 1572
1573 1573 def test_post_create_private_with_set_own_issues_private_permission
1574 1574 role = Role.find(1)
1575 1575 role.remove_permission! :set_issues_private
1576 1576 role.add_permission! :set_own_issues_private
1577 1577
1578 1578 @request.session[:user_id] = 2
1579 1579
1580 1580 assert_difference 'Issue.count' do
1581 1581 post :create, :project_id => 1,
1582 1582 :issue => {:tracker_id => 1,
1583 1583 :subject => 'This is a private issue',
1584 1584 :is_private => '1'}
1585 1585 end
1586 1586 issue = Issue.first(:order => 'id DESC')
1587 1587 assert issue.is_private?
1588 1588 end
1589 1589
1590 1590 def test_post_create_should_send_a_notification
1591 1591 ActionMailer::Base.deliveries.clear
1592 1592 @request.session[:user_id] = 2
1593 1593 assert_difference 'Issue.count' do
1594 1594 post :create, :project_id => 1,
1595 1595 :issue => {:tracker_id => 3,
1596 1596 :subject => 'This is the test_new issue',
1597 1597 :description => 'This is the description',
1598 1598 :priority_id => 5,
1599 1599 :estimated_hours => '',
1600 1600 :custom_field_values => {'2' => 'Value for field 2'}}
1601 1601 end
1602 1602 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
1603 1603
1604 1604 assert_equal 1, ActionMailer::Base.deliveries.size
1605 1605 end
1606 1606
1607 1607 def test_post_create_should_preserve_fields_values_on_validation_failure
1608 1608 @request.session[:user_id] = 2
1609 1609 post :create, :project_id => 1,
1610 1610 :issue => {:tracker_id => 1,
1611 1611 # empty subject
1612 1612 :subject => '',
1613 1613 :description => 'This is a description',
1614 1614 :priority_id => 6,
1615 1615 :custom_field_values => {'1' => 'Oracle', '2' => 'Value for field 2'}}
1616 1616 assert_response :success
1617 1617 assert_template 'new'
1618 1618
1619 1619 assert_tag :textarea, :attributes => { :name => 'issue[description]' },
1620 1620 :content => 'This is a description'
1621 1621 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
1622 1622 :child => { :tag => 'option', :attributes => { :selected => 'selected',
1623 1623 :value => '6' },
1624 1624 :content => 'High' }
1625 1625 # Custom fields
1626 1626 assert_tag :select, :attributes => { :name => 'issue[custom_field_values][1]' },
1627 1627 :child => { :tag => 'option', :attributes => { :selected => 'selected',
1628 1628 :value => 'Oracle' },
1629 1629 :content => 'Oracle' }
1630 1630 assert_tag :input, :attributes => { :name => 'issue[custom_field_values][2]',
1631 1631 :value => 'Value for field 2'}
1632 1632 end
1633 1633
1634 1634 def test_post_create_should_ignore_non_safe_attributes
1635 1635 @request.session[:user_id] = 2
1636 1636 assert_nothing_raised do
1637 1637 post :create, :project_id => 1, :issue => { :tracker => "A param can not be a Tracker" }
1638 1638 end
1639 1639 end
1640 1640
1641 1641 def test_post_create_with_attachment
1642 1642 set_tmp_attachments_directory
1643 1643 @request.session[:user_id] = 2
1644 1644
1645 1645 assert_difference 'Issue.count' do
1646 1646 assert_difference 'Attachment.count' do
1647 1647 post :create, :project_id => 1,
1648 1648 :issue => { :tracker_id => '1', :subject => 'With attachment' },
1649 1649 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'test file'}}
1650 1650 end
1651 1651 end
1652 1652
1653 1653 issue = Issue.first(:order => 'id DESC')
1654 1654 attachment = Attachment.first(:order => 'id DESC')
1655 1655
1656 1656 assert_equal issue, attachment.container
1657 1657 assert_equal 2, attachment.author_id
1658 1658 assert_equal 'testfile.txt', attachment.filename
1659 1659 assert_equal 'text/plain', attachment.content_type
1660 1660 assert_equal 'test file', attachment.description
1661 1661 assert_equal 59, attachment.filesize
1662 1662 assert File.exists?(attachment.diskfile)
1663 1663 assert_equal 59, File.size(attachment.diskfile)
1664 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 1729 context "without workflow privilege" do
1667 1730 setup do
1668 1731 Workflow.delete_all(["role_id = ?", Role.anonymous.id])
1669 1732 Role.anonymous.add_permission! :add_issues, :add_issue_notes
1670 1733 end
1671 1734
1672 1735 context "#new" do
1673 1736 should "propose default status only" do
1674 1737 get :new, :project_id => 1
1675 1738 assert_response :success
1676 1739 assert_template 'new'
1677 1740 assert_tag :tag => 'select',
1678 1741 :attributes => {:name => 'issue[status_id]'},
1679 1742 :children => {:count => 1},
1680 1743 :child => {:tag => 'option', :attributes => {:value => IssueStatus.default.id.to_s}}
1681 1744 end
1682 1745
1683 1746 should "accept default status" do
1684 1747 assert_difference 'Issue.count' do
1685 1748 post :create, :project_id => 1,
1686 1749 :issue => {:tracker_id => 1,
1687 1750 :subject => 'This is an issue',
1688 1751 :status_id => 1}
1689 1752 end
1690 1753 issue = Issue.last(:order => 'id')
1691 1754 assert_equal IssueStatus.default, issue.status
1692 1755 end
1693 1756
1694 1757 should "ignore unauthorized status" do
1695 1758 assert_difference 'Issue.count' do
1696 1759 post :create, :project_id => 1,
1697 1760 :issue => {:tracker_id => 1,
1698 1761 :subject => 'This is an issue',
1699 1762 :status_id => 3}
1700 1763 end
1701 1764 issue = Issue.last(:order => 'id')
1702 1765 assert_equal IssueStatus.default, issue.status
1703 1766 end
1704 1767 end
1705 1768
1706 1769 context "#update" do
1707 1770 should "ignore status change" do
1708 1771 assert_difference 'Journal.count' do
1709 1772 put :update, :id => 1, :notes => 'just trying', :issue => {:status_id => 3}
1710 1773 end
1711 1774 assert_equal 1, Issue.find(1).status_id
1712 1775 end
1713 1776
1714 1777 should "ignore attributes changes" do
1715 1778 assert_difference 'Journal.count' do
1716 1779 put :update, :id => 1, :notes => 'just trying', :issue => {:subject => 'changed', :assigned_to_id => 2}
1717 1780 end
1718 1781 issue = Issue.find(1)
1719 1782 assert_equal "Can't print recipes", issue.subject
1720 1783 assert_nil issue.assigned_to
1721 1784 end
1722 1785 end
1723 1786 end
1724 1787
1725 1788 context "with workflow privilege" do
1726 1789 setup do
1727 1790 Workflow.delete_all(["role_id = ?", Role.anonymous.id])
1728 1791 Workflow.create!(:role => Role.anonymous, :tracker_id => 1, :old_status_id => 1, :new_status_id => 3)
1729 1792 Workflow.create!(:role => Role.anonymous, :tracker_id => 1, :old_status_id => 1, :new_status_id => 4)
1730 1793 Role.anonymous.add_permission! :add_issues, :add_issue_notes
1731 1794 end
1732 1795
1733 1796 context "#update" do
1734 1797 should "accept authorized status" do
1735 1798 assert_difference 'Journal.count' do
1736 1799 put :update, :id => 1, :notes => 'just trying', :issue => {:status_id => 3}
1737 1800 end
1738 1801 assert_equal 3, Issue.find(1).status_id
1739 1802 end
1740 1803
1741 1804 should "ignore unauthorized status" do
1742 1805 assert_difference 'Journal.count' do
1743 1806 put :update, :id => 1, :notes => 'just trying', :issue => {:status_id => 2}
1744 1807 end
1745 1808 assert_equal 1, Issue.find(1).status_id
1746 1809 end
1747 1810
1748 1811 should "accept authorized attributes changes" do
1749 1812 assert_difference 'Journal.count' do
1750 1813 put :update, :id => 1, :notes => 'just trying', :issue => {:assigned_to_id => 2}
1751 1814 end
1752 1815 issue = Issue.find(1)
1753 1816 assert_equal 2, issue.assigned_to_id
1754 1817 end
1755 1818
1756 1819 should "ignore unauthorized attributes changes" do
1757 1820 assert_difference 'Journal.count' do
1758 1821 put :update, :id => 1, :notes => 'just trying', :issue => {:subject => 'changed'}
1759 1822 end
1760 1823 issue = Issue.find(1)
1761 1824 assert_equal "Can't print recipes", issue.subject
1762 1825 end
1763 1826 end
1764 1827
1765 1828 context "and :edit_issues permission" do
1766 1829 setup do
1767 1830 Role.anonymous.add_permission! :add_issues, :edit_issues
1768 1831 end
1769 1832
1770 1833 should "accept authorized status" do
1771 1834 assert_difference 'Journal.count' do
1772 1835 put :update, :id => 1, :notes => 'just trying', :issue => {:status_id => 3}
1773 1836 end
1774 1837 assert_equal 3, Issue.find(1).status_id
1775 1838 end
1776 1839
1777 1840 should "ignore unauthorized status" do
1778 1841 assert_difference 'Journal.count' do
1779 1842 put :update, :id => 1, :notes => 'just trying', :issue => {:status_id => 2}
1780 1843 end
1781 1844 assert_equal 1, Issue.find(1).status_id
1782 1845 end
1783 1846
1784 1847 should "accept authorized attributes changes" do
1785 1848 assert_difference 'Journal.count' do
1786 1849 put :update, :id => 1, :notes => 'just trying', :issue => {:subject => 'changed', :assigned_to_id => 2}
1787 1850 end
1788 1851 issue = Issue.find(1)
1789 1852 assert_equal "changed", issue.subject
1790 1853 assert_equal 2, issue.assigned_to_id
1791 1854 end
1792 1855 end
1793 1856 end
1794 1857
1795 1858 def test_new_as_copy
1796 1859 @request.session[:user_id] = 2
1797 1860 get :new, :project_id => 1, :copy_from => 1
1798 1861
1799 1862 assert_response :success
1800 1863 assert_template 'new'
1801 1864
1802 1865 assert_not_nil assigns(:issue)
1803 1866 orig = Issue.find(1)
1804 1867 assert_equal 1, assigns(:issue).project_id
1805 1868 assert_equal orig.subject, assigns(:issue).subject
1806 1869 assert assigns(:issue).copy?
1807 1870
1808 1871 assert_tag 'form', :attributes => {:id => 'issue-form', :action => '/projects/ecookbook/issues'}
1809 1872 assert_tag 'select', :attributes => {:name => 'issue[project_id]'}
1810 1873 assert_tag 'select', :attributes => {:name => 'issue[project_id]'},
1811 1874 :child => {:tag => 'option', :attributes => {:value => '1', :selected => 'selected'}, :content => 'eCookbook'}
1812 1875 assert_tag 'select', :attributes => {:name => 'issue[project_id]'},
1813 1876 :child => {:tag => 'option', :attributes => {:value => '2', :selected => nil}, :content => 'OnlineStore'}
1814 1877 assert_tag 'input', :attributes => {:name => 'copy_from', :value => '1'}
1815 1878 end
1816 1879
1817 1880 def test_new_as_copy_with_attachments_should_show_copy_attachments_checkbox
1818 1881 @request.session[:user_id] = 2
1819 1882 issue = Issue.find(3)
1820 1883 assert issue.attachments.count > 0
1821 1884 get :new, :project_id => 1, :copy_from => 3
1822 1885
1823 1886 assert_tag 'input', :attributes => {:name => 'copy_attachments', :type => 'checkbox', :checked => 'checked', :value => '1'}
1824 1887 end
1825 1888
1826 1889 def test_new_as_copy_without_attachments_should_not_show_copy_attachments_checkbox
1827 1890 @request.session[:user_id] = 2
1828 1891 issue = Issue.find(3)
1829 1892 issue.attachments.delete_all
1830 1893 get :new, :project_id => 1, :copy_from => 3
1831 1894
1832 1895 assert_no_tag 'input', :attributes => {:name => 'copy_attachments', :type => 'checkbox', :checked => 'checked', :value => '1'}
1833 1896 end
1834 1897
1835 1898 def test_new_as_copy_with_invalid_issue_should_respond_with_404
1836 1899 @request.session[:user_id] = 2
1837 1900 get :new, :project_id => 1, :copy_from => 99999
1838 1901 assert_response 404
1839 1902 end
1840 1903
1841 1904 def test_create_as_copy_on_different_project
1842 1905 @request.session[:user_id] = 2
1843 1906 assert_difference 'Issue.count' do
1844 1907 post :create, :project_id => 1, :copy_from => 1,
1845 1908 :issue => {:project_id => '2', :tracker_id => '3', :status_id => '1', :subject => 'Copy'}
1846 1909
1847 1910 assert_not_nil assigns(:issue)
1848 1911 assert assigns(:issue).copy?
1849 1912 end
1850 1913 issue = Issue.first(:order => 'id DESC')
1851 1914 assert_redirected_to "/issues/#{issue.id}"
1852 1915
1853 1916 assert_equal 2, issue.project_id
1854 1917 assert_equal 3, issue.tracker_id
1855 1918 assert_equal 'Copy', issue.subject
1856 1919 end
1857 1920
1858 1921 def test_create_as_copy_should_copy_attachments
1859 1922 @request.session[:user_id] = 2
1860 1923 issue = Issue.find(3)
1861 1924 count = issue.attachments.count
1862 1925 assert count > 0
1863 1926
1864 1927 assert_difference 'Issue.count' do
1865 1928 assert_difference 'Attachment.count', count do
1866 1929 assert_no_difference 'Journal.count' do
1867 1930 post :create, :project_id => 1, :copy_from => 3,
1868 1931 :issue => {:project_id => '1', :tracker_id => '3', :status_id => '1', :subject => 'Copy with attachments'},
1869 1932 :copy_attachments => '1'
1870 1933 end
1871 1934 end
1872 1935 end
1873 1936 copy = Issue.first(:order => 'id DESC')
1874 1937 assert_equal count, copy.attachments.count
1875 1938 assert_equal issue.attachments.map(&:filename).sort, copy.attachments.map(&:filename).sort
1876 1939 end
1877 1940
1878 1941 def test_create_as_copy_without_copy_attachments_option_should_not_copy_attachments
1879 1942 @request.session[:user_id] = 2
1880 1943 issue = Issue.find(3)
1881 1944 count = issue.attachments.count
1882 1945 assert count > 0
1883 1946
1884 1947 assert_difference 'Issue.count' do
1885 1948 assert_no_difference 'Attachment.count' do
1886 1949 assert_no_difference 'Journal.count' do
1887 1950 post :create, :project_id => 1, :copy_from => 3,
1888 1951 :issue => {:project_id => '1', :tracker_id => '3', :status_id => '1', :subject => 'Copy with attachments'}
1889 1952 end
1890 1953 end
1891 1954 end
1892 1955 copy = Issue.first(:order => 'id DESC')
1893 1956 assert_equal 0, copy.attachments.count
1894 1957 end
1895 1958
1896 1959 def test_create_as_copy_with_attachments_should_add_new_files
1897 1960 @request.session[:user_id] = 2
1898 1961 issue = Issue.find(3)
1899 1962 count = issue.attachments.count
1900 1963 assert count > 0
1901 1964
1902 1965 assert_difference 'Issue.count' do
1903 1966 assert_difference 'Attachment.count', count + 1 do
1904 1967 assert_no_difference 'Journal.count' do
1905 1968 post :create, :project_id => 1, :copy_from => 3,
1906 1969 :issue => {:project_id => '1', :tracker_id => '3', :status_id => '1', :subject => 'Copy with attachments'},
1907 1970 :copy_attachments => '1',
1908 1971 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'test file'}}
1909 1972 end
1910 1973 end
1911 1974 end
1912 1975 copy = Issue.first(:order => 'id DESC')
1913 1976 assert_equal count + 1, copy.attachments.count
1914 1977 end
1915 1978
1916 1979 def test_create_as_copy_with_failure
1917 1980 @request.session[:user_id] = 2
1918 1981 post :create, :project_id => 1, :copy_from => 1,
1919 1982 :issue => {:project_id => '2', :tracker_id => '3', :status_id => '1', :subject => ''}
1920 1983
1921 1984 assert_response :success
1922 1985 assert_template 'new'
1923 1986
1924 1987 assert_not_nil assigns(:issue)
1925 1988 assert assigns(:issue).copy?
1926 1989
1927 1990 assert_tag 'form', :attributes => {:id => 'issue-form', :action => '/projects/ecookbook/issues'}
1928 1991 assert_tag 'select', :attributes => {:name => 'issue[project_id]'}
1929 1992 assert_tag 'select', :attributes => {:name => 'issue[project_id]'},
1930 1993 :child => {:tag => 'option', :attributes => {:value => '1', :selected => nil}, :content => 'eCookbook'}
1931 1994 assert_tag 'select', :attributes => {:name => 'issue[project_id]'},
1932 1995 :child => {:tag => 'option', :attributes => {:value => '2', :selected => 'selected'}, :content => 'OnlineStore'}
1933 1996 assert_tag 'input', :attributes => {:name => 'copy_from', :value => '1'}
1934 1997 end
1935 1998
1936 1999 def test_create_as_copy_on_project_without_permission_should_ignore_target_project
1937 2000 @request.session[:user_id] = 2
1938 2001 assert !User.find(2).member_of?(Project.find(4))
1939 2002
1940 2003 assert_difference 'Issue.count' do
1941 2004 post :create, :project_id => 1, :copy_from => 1,
1942 2005 :issue => {:project_id => '4', :tracker_id => '3', :status_id => '1', :subject => 'Copy'}
1943 2006 end
1944 2007 issue = Issue.first(:order => 'id DESC')
1945 2008 assert_equal 1, issue.project_id
1946 2009 end
1947 2010
1948 2011 def test_get_edit
1949 2012 @request.session[:user_id] = 2
1950 2013 get :edit, :id => 1
1951 2014 assert_response :success
1952 2015 assert_template 'edit'
1953 2016 assert_not_nil assigns(:issue)
1954 2017 assert_equal Issue.find(1), assigns(:issue)
1955 2018
1956 2019 # Be sure we don't display inactive IssuePriorities
1957 2020 assert ! IssuePriority.find(15).active?
1958 2021 assert_no_tag :option, :attributes => {:value => '15'},
1959 2022 :parent => {:tag => 'select', :attributes => {:id => 'issue_priority_id'} }
1960 2023 end
1961 2024
1962 2025 def test_get_edit_should_display_the_time_entry_form_with_log_time_permission
1963 2026 @request.session[:user_id] = 2
1964 2027 Role.find_by_name('Manager').update_attribute :permissions, [:view_issues, :edit_issues, :log_time]
1965 2028
1966 2029 get :edit, :id => 1
1967 2030 assert_tag 'input', :attributes => {:name => 'time_entry[hours]'}
1968 2031 end
1969 2032
1970 2033 def test_get_edit_should_not_display_the_time_entry_form_without_log_time_permission
1971 2034 @request.session[:user_id] = 2
1972 2035 Role.find_by_name('Manager').remove_permission! :log_time
1973 2036
1974 2037 get :edit, :id => 1
1975 2038 assert_no_tag 'input', :attributes => {:name => 'time_entry[hours]'}
1976 2039 end
1977 2040
1978 2041 def test_get_edit_with_params
1979 2042 @request.session[:user_id] = 2
1980 2043 get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 },
1981 2044 :time_entry => { :hours => '2.5', :comments => 'test_get_edit_with_params', :activity_id => TimeEntryActivity.first.id }
1982 2045 assert_response :success
1983 2046 assert_template 'edit'
1984 2047
1985 2048 issue = assigns(:issue)
1986 2049 assert_not_nil issue
1987 2050
1988 2051 assert_equal 5, issue.status_id
1989 2052 assert_tag :select, :attributes => { :name => 'issue[status_id]' },
1990 2053 :child => { :tag => 'option',
1991 2054 :content => 'Closed',
1992 2055 :attributes => { :selected => 'selected' } }
1993 2056
1994 2057 assert_equal 7, issue.priority_id
1995 2058 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
1996 2059 :child => { :tag => 'option',
1997 2060 :content => 'Urgent',
1998 2061 :attributes => { :selected => 'selected' } }
1999 2062
2000 2063 assert_tag :input, :attributes => { :name => 'time_entry[hours]', :value => '2.5' }
2001 2064 assert_tag :select, :attributes => { :name => 'time_entry[activity_id]' },
2002 2065 :child => { :tag => 'option',
2003 2066 :attributes => { :selected => 'selected', :value => TimeEntryActivity.first.id } }
2004 2067 assert_tag :input, :attributes => { :name => 'time_entry[comments]', :value => 'test_get_edit_with_params' }
2005 2068 end
2006 2069
2007 2070 def test_get_edit_with_multi_custom_field
2008 2071 field = CustomField.find(1)
2009 2072 field.update_attribute :multiple, true
2010 2073 issue = Issue.find(1)
2011 2074 issue.custom_field_values = {1 => ['MySQL', 'Oracle']}
2012 2075 issue.save!
2013 2076
2014 2077 @request.session[:user_id] = 2
2015 2078 get :edit, :id => 1
2016 2079 assert_response :success
2017 2080 assert_template 'edit'
2018 2081
2019 2082 assert_tag 'select', :attributes => {:name => 'issue[custom_field_values][1][]', :multiple => 'multiple'}
2020 2083 assert_tag 'select', :attributes => {:name => 'issue[custom_field_values][1][]'},
2021 2084 :child => {:tag => 'option', :attributes => {:value => 'MySQL', :selected => 'selected'}}
2022 2085 assert_tag 'select', :attributes => {:name => 'issue[custom_field_values][1][]'},
2023 2086 :child => {:tag => 'option', :attributes => {:value => 'PostgreSQL', :selected => nil}}
2024 2087 assert_tag 'select', :attributes => {:name => 'issue[custom_field_values][1][]'},
2025 2088 :child => {:tag => 'option', :attributes => {:value => 'Oracle', :selected => 'selected'}}
2026 2089 end
2027 2090
2028 2091 def test_update_edit_form
2029 2092 @request.session[:user_id] = 2
2030 2093 xhr :put, :new, :project_id => 1,
2031 2094 :id => 1,
2032 2095 :issue => {:tracker_id => 2,
2033 2096 :subject => 'This is the test_new issue',
2034 2097 :description => 'This is the description',
2035 2098 :priority_id => 5}
2036 2099 assert_response :success
2037 2100 assert_template 'attributes'
2038 2101
2039 2102 issue = assigns(:issue)
2040 2103 assert_kind_of Issue, issue
2041 2104 assert_equal 1, issue.id
2042 2105 assert_equal 1, issue.project_id
2043 2106 assert_equal 2, issue.tracker_id
2044 2107 assert_equal 'This is the test_new issue', issue.subject
2045 2108 end
2046 2109
2047 2110 def test_update_edit_form_with_project_change
2048 2111 @request.session[:user_id] = 2
2049 2112 xhr :put, :new, :project_id => 1,
2050 2113 :id => 1,
2051 2114 :project_change => '1',
2052 2115 :issue => {:project_id => 2,
2053 2116 :tracker_id => 2,
2054 2117 :subject => 'This is the test_new issue',
2055 2118 :description => 'This is the description',
2056 2119 :priority_id => 5}
2057 2120 assert_response :success
2058 2121 assert_template 'form'
2059 2122
2060 2123 issue = assigns(:issue)
2061 2124 assert_kind_of Issue, issue
2062 2125 assert_equal 1, issue.id
2063 2126 assert_equal 2, issue.project_id
2064 2127 assert_equal 2, issue.tracker_id
2065 2128 assert_equal 'This is the test_new issue', issue.subject
2066 2129 end
2067 2130
2068 2131 def test_update_using_invalid_http_verbs
2069 2132 @request.session[:user_id] = 2
2070 2133 subject = 'Updated by an invalid http verb'
2071 2134
2072 2135 get :update, :id => 1, :issue => {:subject => subject}
2073 2136 assert_not_equal subject, Issue.find(1).subject
2074 2137
2075 2138 post :update, :id => 1, :issue => {:subject => subject}
2076 2139 assert_not_equal subject, Issue.find(1).subject
2077 2140
2078 2141 delete :update, :id => 1, :issue => {:subject => subject}
2079 2142 assert_not_equal subject, Issue.find(1).subject
2080 2143 end
2081 2144
2082 2145 def test_put_update_without_custom_fields_param
2083 2146 @request.session[:user_id] = 2
2084 2147 ActionMailer::Base.deliveries.clear
2085 2148
2086 2149 issue = Issue.find(1)
2087 2150 assert_equal '125', issue.custom_value_for(2).value
2088 2151 old_subject = issue.subject
2089 2152 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
2090 2153
2091 2154 assert_difference('Journal.count') do
2092 2155 assert_difference('JournalDetail.count', 2) do
2093 2156 put :update, :id => 1, :issue => {:subject => new_subject,
2094 2157 :priority_id => '6',
2095 2158 :category_id => '1' # no change
2096 2159 }
2097 2160 end
2098 2161 end
2099 2162 assert_redirected_to :action => 'show', :id => '1'
2100 2163 issue.reload
2101 2164 assert_equal new_subject, issue.subject
2102 2165 # Make sure custom fields were not cleared
2103 2166 assert_equal '125', issue.custom_value_for(2).value
2104 2167
2105 2168 mail = ActionMailer::Base.deliveries.last
2106 2169 assert_kind_of TMail::Mail, mail
2107 2170 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
2108 2171 assert mail.body.include?("Subject changed from #{old_subject} to #{new_subject}")
2109 2172 end
2110 2173
2111 2174 def test_put_update_with_project_change
2112 2175 @request.session[:user_id] = 2
2113 2176 ActionMailer::Base.deliveries.clear
2114 2177
2115 2178 assert_difference('Journal.count') do
2116 2179 assert_difference('JournalDetail.count', 3) do
2117 2180 put :update, :id => 1, :issue => {:project_id => '2',
2118 2181 :tracker_id => '1', # no change
2119 2182 :priority_id => '6',
2120 2183 :category_id => '3'
2121 2184 }
2122 2185 end
2123 2186 end
2124 2187 assert_redirected_to :action => 'show', :id => '1'
2125 2188 issue = Issue.find(1)
2126 2189 assert_equal 2, issue.project_id
2127 2190 assert_equal 1, issue.tracker_id
2128 2191 assert_equal 6, issue.priority_id
2129 2192 assert_equal 3, issue.category_id
2130 2193
2131 2194 mail = ActionMailer::Base.deliveries.last
2132 2195 assert_not_nil mail
2133 2196 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
2134 2197 assert mail.body.include?("Project changed from eCookbook to OnlineStore")
2135 2198 end
2136 2199
2137 2200 def test_put_update_with_tracker_change
2138 2201 @request.session[:user_id] = 2
2139 2202 ActionMailer::Base.deliveries.clear
2140 2203
2141 2204 assert_difference('Journal.count') do
2142 2205 assert_difference('JournalDetail.count', 2) do
2143 2206 put :update, :id => 1, :issue => {:project_id => '1',
2144 2207 :tracker_id => '2',
2145 2208 :priority_id => '6'
2146 2209 }
2147 2210 end
2148 2211 end
2149 2212 assert_redirected_to :action => 'show', :id => '1'
2150 2213 issue = Issue.find(1)
2151 2214 assert_equal 1, issue.project_id
2152 2215 assert_equal 2, issue.tracker_id
2153 2216 assert_equal 6, issue.priority_id
2154 2217 assert_equal 1, issue.category_id
2155 2218
2156 2219 mail = ActionMailer::Base.deliveries.last
2157 2220 assert_not_nil mail
2158 2221 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
2159 2222 assert mail.body.include?("Tracker changed from Bug to Feature request")
2160 2223 end
2161 2224
2162 2225 def test_put_update_with_custom_field_change
2163 2226 @request.session[:user_id] = 2
2164 2227 issue = Issue.find(1)
2165 2228 assert_equal '125', issue.custom_value_for(2).value
2166 2229
2167 2230 assert_difference('Journal.count') do
2168 2231 assert_difference('JournalDetail.count', 3) do
2169 2232 put :update, :id => 1, :issue => {:subject => 'Custom field change',
2170 2233 :priority_id => '6',
2171 2234 :category_id => '1', # no change
2172 2235 :custom_field_values => { '2' => 'New custom value' }
2173 2236 }
2174 2237 end
2175 2238 end
2176 2239 assert_redirected_to :action => 'show', :id => '1'
2177 2240 issue.reload
2178 2241 assert_equal 'New custom value', issue.custom_value_for(2).value
2179 2242
2180 2243 mail = ActionMailer::Base.deliveries.last
2181 2244 assert_kind_of TMail::Mail, mail
2182 2245 assert mail.body.include?("Searchable field changed from 125 to New custom value")
2183 2246 end
2184 2247
2185 2248 def test_put_update_with_multi_custom_field_change
2186 2249 field = CustomField.find(1)
2187 2250 field.update_attribute :multiple, true
2188 2251 issue = Issue.find(1)
2189 2252 issue.custom_field_values = {1 => ['MySQL', 'Oracle']}
2190 2253 issue.save!
2191 2254
2192 2255 @request.session[:user_id] = 2
2193 2256 assert_difference('Journal.count') do
2194 2257 assert_difference('JournalDetail.count', 3) do
2195 2258 put :update, :id => 1,
2196 2259 :issue => {
2197 2260 :subject => 'Custom field change',
2198 2261 :custom_field_values => { '1' => ['', 'Oracle', 'PostgreSQL'] }
2199 2262 }
2200 2263 end
2201 2264 end
2202 2265 assert_redirected_to :action => 'show', :id => '1'
2203 2266 assert_equal ['Oracle', 'PostgreSQL'], Issue.find(1).custom_field_value(1).sort
2204 2267 end
2205 2268
2206 2269 def test_put_update_with_status_and_assignee_change
2207 2270 issue = Issue.find(1)
2208 2271 assert_equal 1, issue.status_id
2209 2272 @request.session[:user_id] = 2
2210 2273 assert_difference('TimeEntry.count', 0) do
2211 2274 put :update,
2212 2275 :id => 1,
2213 2276 :issue => { :status_id => 2, :assigned_to_id => 3 },
2214 2277 :notes => 'Assigned to dlopper',
2215 2278 :time_entry => { :hours => '', :comments => '', :activity_id => TimeEntryActivity.first }
2216 2279 end
2217 2280 assert_redirected_to :action => 'show', :id => '1'
2218 2281 issue.reload
2219 2282 assert_equal 2, issue.status_id
2220 2283 j = Journal.find(:first, :order => 'id DESC')
2221 2284 assert_equal 'Assigned to dlopper', j.notes
2222 2285 assert_equal 2, j.details.size
2223 2286
2224 2287 mail = ActionMailer::Base.deliveries.last
2225 2288 assert mail.body.include?("Status changed from New to Assigned")
2226 2289 # subject should contain the new status
2227 2290 assert mail.subject.include?("(#{ IssueStatus.find(2).name })")
2228 2291 end
2229 2292
2230 2293 def test_put_update_with_note_only
2231 2294 notes = 'Note added by IssuesControllerTest#test_update_with_note_only'
2232 2295 # anonymous user
2233 2296 put :update,
2234 2297 :id => 1,
2235 2298 :notes => notes
2236 2299 assert_redirected_to :action => 'show', :id => '1'
2237 2300 j = Journal.find(:first, :order => 'id DESC')
2238 2301 assert_equal notes, j.notes
2239 2302 assert_equal 0, j.details.size
2240 2303 assert_equal User.anonymous, j.user
2241 2304
2242 2305 mail = ActionMailer::Base.deliveries.last
2243 2306 assert mail.body.include?(notes)
2244 2307 end
2245 2308
2246 2309 def test_put_update_with_note_and_spent_time
2247 2310 @request.session[:user_id] = 2
2248 2311 spent_hours_before = Issue.find(1).spent_hours
2249 2312 assert_difference('TimeEntry.count') do
2250 2313 put :update,
2251 2314 :id => 1,
2252 2315 :notes => '2.5 hours added',
2253 2316 :time_entry => { :hours => '2.5', :comments => 'test_put_update_with_note_and_spent_time', :activity_id => TimeEntryActivity.first.id }
2254 2317 end
2255 2318 assert_redirected_to :action => 'show', :id => '1'
2256 2319
2257 2320 issue = Issue.find(1)
2258 2321
2259 2322 j = Journal.find(:first, :order => 'id DESC')
2260 2323 assert_equal '2.5 hours added', j.notes
2261 2324 assert_equal 0, j.details.size
2262 2325
2263 2326 t = issue.time_entries.find_by_comments('test_put_update_with_note_and_spent_time')
2264 2327 assert_not_nil t
2265 2328 assert_equal 2.5, t.hours
2266 2329 assert_equal spent_hours_before + 2.5, issue.spent_hours
2267 2330 end
2268 2331
2269 2332 def test_put_update_with_attachment_only
2270 2333 set_tmp_attachments_directory
2271 2334
2272 2335 # Delete all fixtured journals, a race condition can occur causing the wrong
2273 2336 # journal to get fetched in the next find.
2274 2337 Journal.delete_all
2275 2338
2276 2339 # anonymous user
2277 2340 assert_difference 'Attachment.count' do
2278 2341 put :update, :id => 1,
2279 2342 :notes => '',
2280 2343 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'test file'}}
2281 2344 end
2282 2345
2283 2346 assert_redirected_to :action => 'show', :id => '1'
2284 2347 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
2285 2348 assert j.notes.blank?
2286 2349 assert_equal 1, j.details.size
2287 2350 assert_equal 'testfile.txt', j.details.first.value
2288 2351 assert_equal User.anonymous, j.user
2289 2352
2290 2353 attachment = Attachment.first(:order => 'id DESC')
2291 2354 assert_equal Issue.find(1), attachment.container
2292 2355 assert_equal User.anonymous, attachment.author
2293 2356 assert_equal 'testfile.txt', attachment.filename
2294 2357 assert_equal 'text/plain', attachment.content_type
2295 2358 assert_equal 'test file', attachment.description
2296 2359 assert_equal 59, attachment.filesize
2297 2360 assert File.exists?(attachment.diskfile)
2298 2361 assert_equal 59, File.size(attachment.diskfile)
2299 2362
2300 2363 mail = ActionMailer::Base.deliveries.last
2301 2364 assert mail.body.include?('testfile.txt')
2302 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 2433 def test_put_update_with_attachment_that_fails_to_save
2305 2434 set_tmp_attachments_directory
2306 2435
2307 2436 # Delete all fixtured journals, a race condition can occur causing the wrong
2308 2437 # journal to get fetched in the next find.
2309 2438 Journal.delete_all
2310 2439
2311 2440 # Mock out the unsaved attachment
2312 2441 Attachment.any_instance.stubs(:create).returns(Attachment.new)
2313 2442
2314 2443 # anonymous user
2315 2444 put :update,
2316 2445 :id => 1,
2317 2446 :notes => '',
2318 2447 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
2319 2448 assert_redirected_to :action => 'show', :id => '1'
2320 2449 assert_equal '1 file(s) could not be saved.', flash[:warning]
2321 2450 end
2322 2451
2323 2452 def test_put_update_with_no_change
2324 2453 issue = Issue.find(1)
2325 2454 issue.journals.clear
2326 2455 ActionMailer::Base.deliveries.clear
2327 2456
2328 2457 put :update,
2329 2458 :id => 1,
2330 2459 :notes => ''
2331 2460 assert_redirected_to :action => 'show', :id => '1'
2332 2461
2333 2462 issue.reload
2334 2463 assert issue.journals.empty?
2335 2464 # No email should be sent
2336 2465 assert ActionMailer::Base.deliveries.empty?
2337 2466 end
2338 2467
2339 2468 def test_put_update_should_send_a_notification
2340 2469 @request.session[:user_id] = 2
2341 2470 ActionMailer::Base.deliveries.clear
2342 2471 issue = Issue.find(1)
2343 2472 old_subject = issue.subject
2344 2473 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
2345 2474
2346 2475 put :update, :id => 1, :issue => {:subject => new_subject,
2347 2476 :priority_id => '6',
2348 2477 :category_id => '1' # no change
2349 2478 }
2350 2479 assert_equal 1, ActionMailer::Base.deliveries.size
2351 2480 end
2352 2481
2353 2482 def test_put_update_with_invalid_spent_time_hours_only
2354 2483 @request.session[:user_id] = 2
2355 2484 notes = 'Note added by IssuesControllerTest#test_post_edit_with_invalid_spent_time'
2356 2485
2357 2486 assert_no_difference('Journal.count') do
2358 2487 put :update,
2359 2488 :id => 1,
2360 2489 :notes => notes,
2361 2490 :time_entry => {"comments"=>"", "activity_id"=>"", "hours"=>"2z"}
2362 2491 end
2363 2492 assert_response :success
2364 2493 assert_template 'edit'
2365 2494
2366 2495 assert_error_tag :descendant => {:content => /Activity can't be blank/}
2367 2496 assert_tag :textarea, :attributes => { :name => 'notes' }, :content => notes
2368 2497 assert_tag :input, :attributes => { :name => 'time_entry[hours]', :value => "2z" }
2369 2498 end
2370 2499
2371 2500 def test_put_update_with_invalid_spent_time_comments_only
2372 2501 @request.session[:user_id] = 2
2373 2502 notes = 'Note added by IssuesControllerTest#test_post_edit_with_invalid_spent_time'
2374 2503
2375 2504 assert_no_difference('Journal.count') do
2376 2505 put :update,
2377 2506 :id => 1,
2378 2507 :notes => notes,
2379 2508 :time_entry => {"comments"=>"this is my comment", "activity_id"=>"", "hours"=>""}
2380 2509 end
2381 2510 assert_response :success
2382 2511 assert_template 'edit'
2383 2512
2384 2513 assert_error_tag :descendant => {:content => /Activity can't be blank/}
2385 2514 assert_error_tag :descendant => {:content => /Hours can't be blank/}
2386 2515 assert_tag :textarea, :attributes => { :name => 'notes' }, :content => notes
2387 2516 assert_tag :input, :attributes => { :name => 'time_entry[comments]', :value => "this is my comment" }
2388 2517 end
2389 2518
2390 2519 def test_put_update_should_allow_fixed_version_to_be_set_to_a_subproject
2391 2520 issue = Issue.find(2)
2392 2521 @request.session[:user_id] = 2
2393 2522
2394 2523 put :update,
2395 2524 :id => issue.id,
2396 2525 :issue => {
2397 2526 :fixed_version_id => 4
2398 2527 }
2399 2528
2400 2529 assert_response :redirect
2401 2530 issue.reload
2402 2531 assert_equal 4, issue.fixed_version_id
2403 2532 assert_not_equal issue.project_id, issue.fixed_version.project_id
2404 2533 end
2405 2534
2406 2535 def test_put_update_should_redirect_back_using_the_back_url_parameter
2407 2536 issue = Issue.find(2)
2408 2537 @request.session[:user_id] = 2
2409 2538
2410 2539 put :update,
2411 2540 :id => issue.id,
2412 2541 :issue => {
2413 2542 :fixed_version_id => 4
2414 2543 },
2415 2544 :back_url => '/issues'
2416 2545
2417 2546 assert_response :redirect
2418 2547 assert_redirected_to '/issues'
2419 2548 end
2420 2549
2421 2550 def test_put_update_should_not_redirect_back_using_the_back_url_parameter_off_the_host
2422 2551 issue = Issue.find(2)
2423 2552 @request.session[:user_id] = 2
2424 2553
2425 2554 put :update,
2426 2555 :id => issue.id,
2427 2556 :issue => {
2428 2557 :fixed_version_id => 4
2429 2558 },
2430 2559 :back_url => 'http://google.com'
2431 2560
2432 2561 assert_response :redirect
2433 2562 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue.id
2434 2563 end
2435 2564
2436 2565 def test_get_bulk_edit
2437 2566 @request.session[:user_id] = 2
2438 2567 get :bulk_edit, :ids => [1, 2]
2439 2568 assert_response :success
2440 2569 assert_template 'bulk_edit'
2441 2570
2442 2571 assert_tag :select, :attributes => {:name => 'issue[project_id]'}
2443 2572 assert_tag :input, :attributes => {:name => 'issue[parent_issue_id]'}
2444 2573
2445 2574 # Project specific custom field, date type
2446 2575 field = CustomField.find(9)
2447 2576 assert !field.is_for_all?
2448 2577 assert_equal 'date', field.field_format
2449 2578 assert_tag :input, :attributes => {:name => 'issue[custom_field_values][9]'}
2450 2579
2451 2580 # System wide custom field
2452 2581 assert CustomField.find(1).is_for_all?
2453 2582 assert_tag :select, :attributes => {:name => 'issue[custom_field_values][1]'}
2454 2583
2455 2584 # Be sure we don't display inactive IssuePriorities
2456 2585 assert ! IssuePriority.find(15).active?
2457 2586 assert_no_tag :option, :attributes => {:value => '15'},
2458 2587 :parent => {:tag => 'select', :attributes => {:id => 'issue_priority_id'} }
2459 2588 end
2460 2589
2461 2590 def test_get_bulk_edit_on_different_projects
2462 2591 @request.session[:user_id] = 2
2463 2592 get :bulk_edit, :ids => [1, 2, 6]
2464 2593 assert_response :success
2465 2594 assert_template 'bulk_edit'
2466 2595
2467 2596 # Can not set issues from different projects as children of an issue
2468 2597 assert_no_tag :input, :attributes => {:name => 'issue[parent_issue_id]'}
2469 2598
2470 2599 # Project specific custom field, date type
2471 2600 field = CustomField.find(9)
2472 2601 assert !field.is_for_all?
2473 2602 assert !field.project_ids.include?(Issue.find(6).project_id)
2474 2603 assert_no_tag :input, :attributes => {:name => 'issue[custom_field_values][9]'}
2475 2604 end
2476 2605
2477 2606 def test_get_bulk_edit_with_user_custom_field
2478 2607 field = IssueCustomField.create!(:name => 'Tester', :field_format => 'user', :is_for_all => true)
2479 2608
2480 2609 @request.session[:user_id] = 2
2481 2610 get :bulk_edit, :ids => [1, 2]
2482 2611 assert_response :success
2483 2612 assert_template 'bulk_edit'
2484 2613
2485 2614 assert_tag :select,
2486 2615 :attributes => {:name => "issue[custom_field_values][#{field.id}]"},
2487 2616 :children => {
2488 2617 :only => {:tag => 'option'},
2489 2618 :count => Project.find(1).users.count + 1
2490 2619 }
2491 2620 end
2492 2621
2493 2622 def test_get_bulk_edit_with_version_custom_field
2494 2623 field = IssueCustomField.create!(:name => 'Affected version', :field_format => 'version', :is_for_all => true)
2495 2624
2496 2625 @request.session[:user_id] = 2
2497 2626 get :bulk_edit, :ids => [1, 2]
2498 2627 assert_response :success
2499 2628 assert_template 'bulk_edit'
2500 2629
2501 2630 assert_tag :select,
2502 2631 :attributes => {:name => "issue[custom_field_values][#{field.id}]"},
2503 2632 :children => {
2504 2633 :only => {:tag => 'option'},
2505 2634 :count => Project.find(1).shared_versions.count + 1
2506 2635 }
2507 2636 end
2508 2637
2509 2638 def test_get_bulk_edit_with_multi_custom_field
2510 2639 field = CustomField.find(1)
2511 2640 field.update_attribute :multiple, true
2512 2641
2513 2642 @request.session[:user_id] = 2
2514 2643 get :bulk_edit, :ids => [1, 2]
2515 2644 assert_response :success
2516 2645 assert_template 'bulk_edit'
2517 2646
2518 2647 assert_tag :select,
2519 2648 :attributes => {:name => "issue[custom_field_values][1][]"},
2520 2649 :children => {
2521 2650 :only => {:tag => 'option'},
2522 2651 :count => 3
2523 2652 }
2524 2653 end
2525 2654
2526 2655 def test_bulk_edit_should_only_propose_statuses_allowed_for_all_issues
2527 2656 Workflow.delete_all
2528 2657 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 1)
2529 2658 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 3)
2530 2659 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 4)
2531 2660 Workflow.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :new_status_id => 1)
2532 2661 Workflow.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :new_status_id => 3)
2533 2662 Workflow.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :new_status_id => 5)
2534 2663 @request.session[:user_id] = 2
2535 2664 get :bulk_edit, :ids => [1, 2]
2536 2665
2537 2666 assert_response :success
2538 2667 statuses = assigns(:available_statuses)
2539 2668 assert_not_nil statuses
2540 2669 assert_equal [1, 3], statuses.map(&:id).sort
2541 2670
2542 2671 assert_tag 'select', :attributes => {:name => 'issue[status_id]'},
2543 2672 :children => {:count => 3} # 2 statuses + "no change" option
2544 2673 end
2545 2674
2546 2675 def test_bulk_update
2547 2676 @request.session[:user_id] = 2
2548 2677 # update issues priority
2549 2678 post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing',
2550 2679 :issue => {:priority_id => 7,
2551 2680 :assigned_to_id => '',
2552 2681 :custom_field_values => {'2' => ''}}
2553 2682
2554 2683 assert_response 302
2555 2684 # check that the issues were updated
2556 2685 assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id}
2557 2686
2558 2687 issue = Issue.find(1)
2559 2688 journal = issue.journals.find(:first, :order => 'created_on DESC')
2560 2689 assert_equal '125', issue.custom_value_for(2).value
2561 2690 assert_equal 'Bulk editing', journal.notes
2562 2691 assert_equal 1, journal.details.size
2563 2692 end
2564 2693
2565 2694 def test_bulk_update_with_group_assignee
2566 2695 group = Group.find(11)
2567 2696 project = Project.find(1)
2568 2697 project.members << Member.new(:principal => group, :roles => [Role.first])
2569 2698
2570 2699 @request.session[:user_id] = 2
2571 2700 # update issues assignee
2572 2701 post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing',
2573 2702 :issue => {:priority_id => '',
2574 2703 :assigned_to_id => group.id,
2575 2704 :custom_field_values => {'2' => ''}}
2576 2705
2577 2706 assert_response 302
2578 2707 assert_equal [group, group], Issue.find_all_by_id([1, 2]).collect {|i| i.assigned_to}
2579 2708 end
2580 2709
2581 2710 def test_bulk_update_on_different_projects
2582 2711 @request.session[:user_id] = 2
2583 2712 # update issues priority
2584 2713 post :bulk_update, :ids => [1, 2, 6], :notes => 'Bulk editing',
2585 2714 :issue => {:priority_id => 7,
2586 2715 :assigned_to_id => '',
2587 2716 :custom_field_values => {'2' => ''}}
2588 2717
2589 2718 assert_response 302
2590 2719 # check that the issues were updated
2591 2720 assert_equal [7, 7, 7], Issue.find([1,2,6]).map(&:priority_id)
2592 2721
2593 2722 issue = Issue.find(1)
2594 2723 journal = issue.journals.find(:first, :order => 'created_on DESC')
2595 2724 assert_equal '125', issue.custom_value_for(2).value
2596 2725 assert_equal 'Bulk editing', journal.notes
2597 2726 assert_equal 1, journal.details.size
2598 2727 end
2599 2728
2600 2729 def test_bulk_update_on_different_projects_without_rights
2601 2730 @request.session[:user_id] = 3
2602 2731 user = User.find(3)
2603 2732 action = { :controller => "issues", :action => "bulk_update" }
2604 2733 assert user.allowed_to?(action, Issue.find(1).project)
2605 2734 assert ! user.allowed_to?(action, Issue.find(6).project)
2606 2735 post :bulk_update, :ids => [1, 6], :notes => 'Bulk should fail',
2607 2736 :issue => {:priority_id => 7,
2608 2737 :assigned_to_id => '',
2609 2738 :custom_field_values => {'2' => ''}}
2610 2739 assert_response 403
2611 2740 assert_not_equal "Bulk should fail", Journal.last.notes
2612 2741 end
2613 2742
2614 2743 def test_bullk_update_should_send_a_notification
2615 2744 @request.session[:user_id] = 2
2616 2745 ActionMailer::Base.deliveries.clear
2617 2746 post(:bulk_update,
2618 2747 {
2619 2748 :ids => [1, 2],
2620 2749 :notes => 'Bulk editing',
2621 2750 :issue => {
2622 2751 :priority_id => 7,
2623 2752 :assigned_to_id => '',
2624 2753 :custom_field_values => {'2' => ''}
2625 2754 }
2626 2755 })
2627 2756
2628 2757 assert_response 302
2629 2758 assert_equal 2, ActionMailer::Base.deliveries.size
2630 2759 end
2631 2760
2632 2761 def test_bulk_update_project
2633 2762 @request.session[:user_id] = 2
2634 2763 post :bulk_update, :ids => [1, 2], :issue => {:project_id => '2'}
2635 2764 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook'
2636 2765 # Issues moved to project 2
2637 2766 assert_equal 2, Issue.find(1).project_id
2638 2767 assert_equal 2, Issue.find(2).project_id
2639 2768 # No tracker change
2640 2769 assert_equal 1, Issue.find(1).tracker_id
2641 2770 assert_equal 2, Issue.find(2).tracker_id
2642 2771 end
2643 2772
2644 2773 def test_bulk_update_project_on_single_issue_should_follow_when_needed
2645 2774 @request.session[:user_id] = 2
2646 2775 post :bulk_update, :id => 1, :issue => {:project_id => '2'}, :follow => '1'
2647 2776 assert_redirected_to '/issues/1'
2648 2777 end
2649 2778
2650 2779 def test_bulk_update_project_on_multiple_issues_should_follow_when_needed
2651 2780 @request.session[:user_id] = 2
2652 2781 post :bulk_update, :id => [1, 2], :issue => {:project_id => '2'}, :follow => '1'
2653 2782 assert_redirected_to '/projects/onlinestore/issues'
2654 2783 end
2655 2784
2656 2785 def test_bulk_update_tracker
2657 2786 @request.session[:user_id] = 2
2658 2787 post :bulk_update, :ids => [1, 2], :issue => {:tracker_id => '2'}
2659 2788 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook'
2660 2789 assert_equal 2, Issue.find(1).tracker_id
2661 2790 assert_equal 2, Issue.find(2).tracker_id
2662 2791 end
2663 2792
2664 2793 def test_bulk_update_status
2665 2794 @request.session[:user_id] = 2
2666 2795 # update issues priority
2667 2796 post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing status',
2668 2797 :issue => {:priority_id => '',
2669 2798 :assigned_to_id => '',
2670 2799 :status_id => '5'}
2671 2800
2672 2801 assert_response 302
2673 2802 issue = Issue.find(1)
2674 2803 assert issue.closed?
2675 2804 end
2676 2805
2677 2806 def test_bulk_update_priority
2678 2807 @request.session[:user_id] = 2
2679 2808 post :bulk_update, :ids => [1, 2], :issue => {:priority_id => 6}
2680 2809
2681 2810 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook'
2682 2811 assert_equal 6, Issue.find(1).priority_id
2683 2812 assert_equal 6, Issue.find(2).priority_id
2684 2813 end
2685 2814
2686 2815 def test_bulk_update_with_notes
2687 2816 @request.session[:user_id] = 2
2688 2817 post :bulk_update, :ids => [1, 2], :notes => 'Moving two issues'
2689 2818
2690 2819 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook'
2691 2820 assert_equal 'Moving two issues', Issue.find(1).journals.sort_by(&:id).last.notes
2692 2821 assert_equal 'Moving two issues', Issue.find(2).journals.sort_by(&:id).last.notes
2693 2822 end
2694 2823
2695 2824 def test_bulk_update_parent_id
2696 2825 @request.session[:user_id] = 2
2697 2826 post :bulk_update, :ids => [1, 3],
2698 2827 :notes => 'Bulk editing parent',
2699 2828 :issue => {:priority_id => '', :assigned_to_id => '', :status_id => '', :parent_issue_id => '2'}
2700 2829
2701 2830 assert_response 302
2702 2831 parent = Issue.find(2)
2703 2832 assert_equal parent.id, Issue.find(1).parent_id
2704 2833 assert_equal parent.id, Issue.find(3).parent_id
2705 2834 assert_equal [1, 3], parent.children.collect(&:id).sort
2706 2835 end
2707 2836
2708 2837 def test_bulk_update_custom_field
2709 2838 @request.session[:user_id] = 2
2710 2839 # update issues priority
2711 2840 post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing custom field',
2712 2841 :issue => {:priority_id => '',
2713 2842 :assigned_to_id => '',
2714 2843 :custom_field_values => {'2' => '777'}}
2715 2844
2716 2845 assert_response 302
2717 2846
2718 2847 issue = Issue.find(1)
2719 2848 journal = issue.journals.find(:first, :order => 'created_on DESC')
2720 2849 assert_equal '777', issue.custom_value_for(2).value
2721 2850 assert_equal 1, journal.details.size
2722 2851 assert_equal '125', journal.details.first.old_value
2723 2852 assert_equal '777', journal.details.first.value
2724 2853 end
2725 2854
2726 2855 def test_bulk_update_multi_custom_field
2727 2856 field = CustomField.find(1)
2728 2857 field.update_attribute :multiple, true
2729 2858
2730 2859 @request.session[:user_id] = 2
2731 2860 post :bulk_update, :ids => [1, 2, 3], :notes => 'Bulk editing multi custom field',
2732 2861 :issue => {:priority_id => '',
2733 2862 :assigned_to_id => '',
2734 2863 :custom_field_values => {'1' => ['MySQL', 'Oracle']}}
2735 2864
2736 2865 assert_response 302
2737 2866
2738 2867 assert_equal ['MySQL', 'Oracle'], Issue.find(1).custom_field_value(1).sort
2739 2868 assert_equal ['MySQL', 'Oracle'], Issue.find(3).custom_field_value(1).sort
2740 2869 # the custom field is not associated with the issue tracker
2741 2870 assert_nil Issue.find(2).custom_field_value(1)
2742 2871 end
2743 2872
2744 2873 def test_bulk_update_unassign
2745 2874 assert_not_nil Issue.find(2).assigned_to
2746 2875 @request.session[:user_id] = 2
2747 2876 # unassign issues
2748 2877 post :bulk_update, :ids => [1, 2], :notes => 'Bulk unassigning', :issue => {:assigned_to_id => 'none'}
2749 2878 assert_response 302
2750 2879 # check that the issues were updated
2751 2880 assert_nil Issue.find(2).assigned_to
2752 2881 end
2753 2882
2754 2883 def test_post_bulk_update_should_allow_fixed_version_to_be_set_to_a_subproject
2755 2884 @request.session[:user_id] = 2
2756 2885
2757 2886 post :bulk_update, :ids => [1,2], :issue => {:fixed_version_id => 4}
2758 2887
2759 2888 assert_response :redirect
2760 2889 issues = Issue.find([1,2])
2761 2890 issues.each do |issue|
2762 2891 assert_equal 4, issue.fixed_version_id
2763 2892 assert_not_equal issue.project_id, issue.fixed_version.project_id
2764 2893 end
2765 2894 end
2766 2895
2767 2896 def test_post_bulk_update_should_redirect_back_using_the_back_url_parameter
2768 2897 @request.session[:user_id] = 2
2769 2898 post :bulk_update, :ids => [1,2], :back_url => '/issues'
2770 2899
2771 2900 assert_response :redirect
2772 2901 assert_redirected_to '/issues'
2773 2902 end
2774 2903
2775 2904 def test_post_bulk_update_should_not_redirect_back_using_the_back_url_parameter_off_the_host
2776 2905 @request.session[:user_id] = 2
2777 2906 post :bulk_update, :ids => [1,2], :back_url => 'http://google.com'
2778 2907
2779 2908 assert_response :redirect
2780 2909 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => Project.find(1).identifier
2781 2910 end
2782 2911
2783 2912 def test_bulk_copy_to_another_project
2784 2913 @request.session[:user_id] = 2
2785 2914 assert_difference 'Issue.count', 2 do
2786 2915 assert_no_difference 'Project.find(1).issues.count' do
2787 2916 post :bulk_update, :ids => [1, 2], :issue => {:project_id => '2'}, :copy => '1'
2788 2917 end
2789 2918 end
2790 2919 assert_redirected_to '/projects/ecookbook/issues'
2791 2920 end
2792 2921
2793 2922 def test_bulk_copy_should_allow_not_changing_the_issue_attributes
2794 2923 @request.session[:user_id] = 2
2795 2924 issue_before_move = Issue.find(1)
2796 2925 assert_difference 'Issue.count', 1 do
2797 2926 assert_no_difference 'Project.find(1).issues.count' do
2798 2927 post :bulk_update, :ids => [1], :copy => '1',
2799 2928 :issue => {
2800 2929 :project_id => '2', :tracker_id => '', :assigned_to_id => '',
2801 2930 :status_id => '', :start_date => '', :due_date => ''
2802 2931 }
2803 2932 end
2804 2933 end
2805 2934 issue_after_move = Issue.first(:order => 'id desc', :conditions => {:project_id => 2})
2806 2935 assert_equal issue_before_move.tracker_id, issue_after_move.tracker_id
2807 2936 assert_equal issue_before_move.status_id, issue_after_move.status_id
2808 2937 assert_equal issue_before_move.assigned_to_id, issue_after_move.assigned_to_id
2809 2938 end
2810 2939
2811 2940 def test_bulk_copy_should_allow_changing_the_issue_attributes
2812 2941 # Fixes random test failure with Mysql
2813 2942 # where Issue.all(:limit => 2, :order => 'id desc', :conditions => {:project_id => 2})
2814 2943 # doesn't return the expected results
2815 2944 Issue.delete_all("project_id=2")
2816 2945
2817 2946 @request.session[:user_id] = 2
2818 2947 assert_difference 'Issue.count', 2 do
2819 2948 assert_no_difference 'Project.find(1).issues.count' do
2820 2949 post :bulk_update, :ids => [1, 2], :copy => '1',
2821 2950 :issue => {
2822 2951 :project_id => '2', :tracker_id => '', :assigned_to_id => '4',
2823 2952 :status_id => '3', :start_date => '2009-12-01', :due_date => '2009-12-31'
2824 2953 }
2825 2954 end
2826 2955 end
2827 2956
2828 2957 copied_issues = Issue.all(:limit => 2, :order => 'id desc', :conditions => {:project_id => 2})
2829 2958 assert_equal 2, copied_issues.size
2830 2959 copied_issues.each do |issue|
2831 2960 assert_equal 2, issue.project_id, "Project is incorrect"
2832 2961 assert_equal 4, issue.assigned_to_id, "Assigned to is incorrect"
2833 2962 assert_equal 3, issue.status_id, "Status is incorrect"
2834 2963 assert_equal '2009-12-01', issue.start_date.to_s, "Start date is incorrect"
2835 2964 assert_equal '2009-12-31', issue.due_date.to_s, "Due date is incorrect"
2836 2965 end
2837 2966 end
2838 2967
2839 2968 def test_bulk_copy_should_allow_adding_a_note
2840 2969 @request.session[:user_id] = 2
2841 2970 assert_difference 'Issue.count', 1 do
2842 2971 post :bulk_update, :ids => [1], :copy => '1',
2843 2972 :notes => 'Copying one issue',
2844 2973 :issue => {
2845 2974 :project_id => '', :tracker_id => '', :assigned_to_id => '4',
2846 2975 :status_id => '3', :start_date => '2009-12-01', :due_date => '2009-12-31'
2847 2976 }
2848 2977 end
2849 2978
2850 2979 issue = Issue.first(:order => 'id DESC')
2851 2980 assert_equal 1, issue.journals.size
2852 2981 journal = issue.journals.first
2853 2982 assert_equal 0, journal.details.size
2854 2983 assert_equal 'Copying one issue', journal.notes
2855 2984 end
2856 2985
2857 2986 def test_bulk_copy_to_another_project_should_follow_when_needed
2858 2987 @request.session[:user_id] = 2
2859 2988 post :bulk_update, :ids => [1], :copy => '1', :issue => {:project_id => 2}, :follow => '1'
2860 2989 issue = Issue.first(:order => 'id DESC')
2861 2990 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
2862 2991 end
2863 2992
2864 2993 def test_destroy_issue_with_no_time_entries
2865 2994 assert_nil TimeEntry.find_by_issue_id(2)
2866 2995 @request.session[:user_id] = 2
2867 2996
2868 2997 assert_difference 'Issue.count', -1 do
2869 2998 delete :destroy, :id => 2
2870 2999 end
2871 3000 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
2872 3001 assert_nil Issue.find_by_id(2)
2873 3002 end
2874 3003
2875 3004 def test_destroy_issues_with_time_entries
2876 3005 @request.session[:user_id] = 2
2877 3006
2878 3007 assert_no_difference 'Issue.count' do
2879 3008 delete :destroy, :ids => [1, 3]
2880 3009 end
2881 3010 assert_response :success
2882 3011 assert_template 'destroy'
2883 3012 assert_not_nil assigns(:hours)
2884 3013 assert Issue.find_by_id(1) && Issue.find_by_id(3)
2885 3014 assert_tag 'form',
2886 3015 :descendant => {:tag => 'input', :attributes => {:name => '_method', :value => 'delete'}}
2887 3016 end
2888 3017
2889 3018 def test_destroy_issues_and_destroy_time_entries
2890 3019 @request.session[:user_id] = 2
2891 3020
2892 3021 assert_difference 'Issue.count', -2 do
2893 3022 assert_difference 'TimeEntry.count', -3 do
2894 3023 delete :destroy, :ids => [1, 3], :todo => 'destroy'
2895 3024 end
2896 3025 end
2897 3026 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
2898 3027 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
2899 3028 assert_nil TimeEntry.find_by_id([1, 2])
2900 3029 end
2901 3030
2902 3031 def test_destroy_issues_and_assign_time_entries_to_project
2903 3032 @request.session[:user_id] = 2
2904 3033
2905 3034 assert_difference 'Issue.count', -2 do
2906 3035 assert_no_difference 'TimeEntry.count' do
2907 3036 delete :destroy, :ids => [1, 3], :todo => 'nullify'
2908 3037 end
2909 3038 end
2910 3039 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
2911 3040 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
2912 3041 assert_nil TimeEntry.find(1).issue_id
2913 3042 assert_nil TimeEntry.find(2).issue_id
2914 3043 end
2915 3044
2916 3045 def test_destroy_issues_and_reassign_time_entries_to_another_issue
2917 3046 @request.session[:user_id] = 2
2918 3047
2919 3048 assert_difference 'Issue.count', -2 do
2920 3049 assert_no_difference 'TimeEntry.count' do
2921 3050 delete :destroy, :ids => [1, 3], :todo => 'reassign', :reassign_to_id => 2
2922 3051 end
2923 3052 end
2924 3053 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
2925 3054 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
2926 3055 assert_equal 2, TimeEntry.find(1).issue_id
2927 3056 assert_equal 2, TimeEntry.find(2).issue_id
2928 3057 end
2929 3058
2930 3059 def test_destroy_issues_from_different_projects
2931 3060 @request.session[:user_id] = 2
2932 3061
2933 3062 assert_difference 'Issue.count', -3 do
2934 3063 delete :destroy, :ids => [1, 2, 6], :todo => 'destroy'
2935 3064 end
2936 3065 assert_redirected_to :controller => 'issues', :action => 'index'
2937 3066 assert !(Issue.find_by_id(1) || Issue.find_by_id(2) || Issue.find_by_id(6))
2938 3067 end
2939 3068
2940 3069 def test_destroy_parent_and_child_issues
2941 3070 parent = Issue.generate!(:project_id => 1, :tracker_id => 1)
2942 3071 child = Issue.generate!(:project_id => 1, :tracker_id => 1, :parent_issue_id => parent.id)
2943 3072 assert child.is_descendant_of?(parent.reload)
2944 3073
2945 3074 @request.session[:user_id] = 2
2946 3075 assert_difference 'Issue.count', -2 do
2947 3076 delete :destroy, :ids => [parent.id, child.id], :todo => 'destroy'
2948 3077 end
2949 3078 assert_response 302
2950 3079 end
2951 3080
2952 3081 def test_default_search_scope
2953 3082 get :index
2954 3083 assert_tag :div, :attributes => {:id => 'quick-search'},
2955 3084 :child => {:tag => 'form',
2956 3085 :child => {:tag => 'input', :attributes => {:name => 'issues', :type => 'hidden', :value => '1'}}}
2957 3086 end
2958 3087 end
@@ -1,207 +1,232
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../test_helper', __FILE__)
19 19 require 'issues_controller'
20 20
21 21 class IssuesControllerTransactionTest < ActionController::TestCase
22 22 fixtures :projects,
23 23 :users,
24 24 :roles,
25 25 :members,
26 26 :member_roles,
27 27 :issues,
28 28 :issue_statuses,
29 29 :versions,
30 30 :trackers,
31 31 :projects_trackers,
32 32 :issue_categories,
33 33 :enabled_modules,
34 34 :enumerations,
35 35 :attachments,
36 36 :workflows,
37 37 :custom_fields,
38 38 :custom_values,
39 39 :custom_fields_projects,
40 40 :custom_fields_trackers,
41 41 :time_entries,
42 42 :journals,
43 43 :journal_details,
44 44 :queries
45 45
46 46 self.use_transactional_fixtures = false
47 47
48 48 def setup
49 49 @controller = IssuesController.new
50 50 @request = ActionController::TestRequest.new
51 51 @response = ActionController::TestResponse.new
52 52 User.current = nil
53 53 end
54 54
55 55 def test_update_stale_issue_should_not_update_the_issue
56 56 issue = Issue.find(2)
57 57 @request.session[:user_id] = 2
58 58
59 59 assert_no_difference 'Journal.count' do
60 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 88 put :update,
63 89 :id => issue.id,
64 90 :issue => {
65 91 :fixed_version_id => 4,
66 92 :lock_version => (issue.lock_version - 1)
67 93 },
68 94 :notes => 'My notes',
69 95 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}},
70 96 :time_entry => { :hours => '2.5', :comments => '', :activity_id => TimeEntryActivity.first.id }
71 97 end
72 98 end
73 99 end
74 100
75 101 assert_response :success
76 102 assert_template 'edit'
77 assert_tag 'div', :attributes => {:class => 'conflict'}
78 assert_tag 'input', :attributes => {:name => 'conflict_resolution', :value => 'overwrite'}
79 assert_tag 'input', :attributes => {:name => 'conflict_resolution', :value => 'add_notes'}
80 assert_tag 'input', :attributes => {:name => 'conflict_resolution', :value => 'cancel'}
103 attachment = Attachment.first(:order => 'id DESC')
104 assert_tag 'input', :attributes => {:name => 'attachments[p0][token]', :value => attachment.token}
105 assert_tag 'span', :content => /testfile.txt/
81 106 end
82 107
83 108 def test_update_stale_issue_without_notes_should_not_show_add_notes_option
84 109 issue = Issue.find(2)
85 110 @request.session[:user_id] = 2
86 111
87 112 put :update, :id => issue.id,
88 113 :issue => {
89 114 :fixed_version_id => 4,
90 115 :lock_version => (issue.lock_version - 1)
91 116 },
92 117 :notes => ''
93 118
94 119 assert_tag 'div', :attributes => {:class => 'conflict'}
95 120 assert_tag 'input', :attributes => {:name => 'conflict_resolution', :value => 'overwrite'}
96 121 assert_no_tag 'input', :attributes => {:name => 'conflict_resolution', :value => 'add_notes'}
97 122 assert_tag 'input', :attributes => {:name => 'conflict_resolution', :value => 'cancel'}
98 123
99 124 end
100 125
101 126 def test_update_stale_issue_should_show_conflicting_journals
102 127 @request.session[:user_id] = 2
103 128
104 129 put :update, :id => 1,
105 130 :issue => {
106 131 :fixed_version_id => 4,
107 132 :lock_version => 2
108 133 },
109 134 :notes => '',
110 135 :last_journal_id => 1
111 136
112 137 assert_not_nil assigns(:conflict_journals)
113 138 assert_equal 1, assigns(:conflict_journals).size
114 139 assert_equal 2, assigns(:conflict_journals).first.id
115 140 assert_tag 'div', :attributes => {:class => 'conflict'},
116 141 :descendant => {:content => /Some notes with Redmine links/}
117 142 end
118 143
119 144 def test_update_stale_issue_without_previous_journal_should_show_all_journals
120 145 @request.session[:user_id] = 2
121 146
122 147 put :update, :id => 1,
123 148 :issue => {
124 149 :fixed_version_id => 4,
125 150 :lock_version => 2
126 151 },
127 152 :notes => '',
128 153 :last_journal_id => ''
129 154
130 155 assert_not_nil assigns(:conflict_journals)
131 156 assert_equal 2, assigns(:conflict_journals).size
132 157 assert_tag 'div', :attributes => {:class => 'conflict'},
133 158 :descendant => {:content => /Some notes with Redmine links/}
134 159 assert_tag 'div', :attributes => {:class => 'conflict'},
135 160 :descendant => {:content => /Journal notes/}
136 161 end
137 162
138 163 def test_update_stale_issue_with_overwrite_conflict_resolution_should_update
139 164 @request.session[:user_id] = 2
140 165
141 166 assert_difference 'Journal.count' do
142 167 put :update, :id => 1,
143 168 :issue => {
144 169 :fixed_version_id => 4,
145 170 :lock_version => 2
146 171 },
147 172 :notes => 'overwrite_conflict_resolution',
148 173 :conflict_resolution => 'overwrite'
149 174 end
150 175
151 176 assert_response 302
152 177 issue = Issue.find(1)
153 178 assert_equal 4, issue.fixed_version_id
154 179 journal = Journal.first(:order => 'id DESC')
155 180 assert_equal 'overwrite_conflict_resolution', journal.notes
156 181 assert journal.details.any?
157 182 end
158 183
159 184 def test_update_stale_issue_with_add_notes_conflict_resolution_should_update
160 185 @request.session[:user_id] = 2
161 186
162 187 assert_difference 'Journal.count' do
163 188 put :update, :id => 1,
164 189 :issue => {
165 190 :fixed_version_id => 4,
166 191 :lock_version => 2
167 192 },
168 193 :notes => 'add_notes_conflict_resolution',
169 194 :conflict_resolution => 'add_notes'
170 195 end
171 196
172 197 assert_response 302
173 198 issue = Issue.find(1)
174 199 assert_nil issue.fixed_version_id
175 200 journal = Journal.first(:order => 'id DESC')
176 201 assert_equal 'add_notes_conflict_resolution', journal.notes
177 202 assert journal.details.empty?
178 203 end
179 204
180 205 def test_update_stale_issue_with_cancel_conflict_resolution_should_redirect_without_updating
181 206 @request.session[:user_id] = 2
182 207
183 208 assert_no_difference 'Journal.count' do
184 209 put :update, :id => 1,
185 210 :issue => {
186 211 :fixed_version_id => 4,
187 212 :lock_version => 2
188 213 },
189 214 :notes => 'add_notes_conflict_resolution',
190 215 :conflict_resolution => 'cancel'
191 216 end
192 217
193 218 assert_redirected_to '/issues/1'
194 219 issue = Issue.find(1)
195 220 assert_nil issue.fixed_version_id
196 221 end
197 222
198 223 def test_index_should_rescue_invalid_sql_query
199 224 Query.any_instance.stubs(:statement).returns("INVALID STATEMENT")
200 225
201 226 get :index
202 227 assert_response 500
203 228 assert_tag 'p', :content => /An error occurred/
204 229 assert_nil session[:query]
205 230 assert_nil session[:issues_index_sort]
206 231 end
207 232 end
@@ -1,203 +1,207
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 4 # Copyright (C) 2006-2011 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 require File.expand_path('../../test_helper', __FILE__)
21 21
22 22 class AttachmentTest < ActiveSupport::TestCase
23 23 fixtures :users, :projects, :roles, :members, :member_roles,
24 24 :enabled_modules, :issues, :trackers, :attachments
25 25
26 26 class MockFile
27 27 attr_reader :original_filename, :content_type, :content, :size
28 28
29 29 def initialize(attributes)
30 30 @original_filename = attributes[:original_filename]
31 31 @content_type = attributes[:content_type]
32 32 @content = attributes[:content] || "Content"
33 33 @size = content.size
34 34 end
35 35 end
36 36
37 37 def setup
38 38 set_tmp_attachments_directory
39 39 end
40 40
41 def test_container_for_new_attachment_should_be_nil
42 assert_nil Attachment.new.container
43 end
44
41 45 def test_create
42 46 a = Attachment.new(:container => Issue.find(1),
43 47 :file => uploaded_test_file("testfile.txt", "text/plain"),
44 48 :author => User.find(1))
45 49 assert a.save
46 50 assert_equal 'testfile.txt', a.filename
47 51 assert_equal 59, a.filesize
48 52 assert_equal 'text/plain', a.content_type
49 53 assert_equal 0, a.downloads
50 54 assert_equal '1478adae0d4eb06d35897518540e25d6', a.digest
51 55 assert File.exist?(a.diskfile)
52 56 assert_equal 59, File.size(a.diskfile)
53 57 end
54 58
55 59 def test_size_should_be_validated_for_new_file
56 60 with_settings :attachment_max_size => 0 do
57 61 a = Attachment.new(:container => Issue.find(1),
58 62 :file => uploaded_test_file("testfile.txt", "text/plain"),
59 63 :author => User.find(1))
60 64 assert !a.save
61 65 end
62 66 end
63 67
64 68 def test_size_should_not_be_validated_when_copying
65 69 a = Attachment.create!(:container => Issue.find(1),
66 70 :file => uploaded_test_file("testfile.txt", "text/plain"),
67 71 :author => User.find(1))
68 72 with_settings :attachment_max_size => 0 do
69 73 copy = a.copy
70 74 assert copy.save
71 75 end
72 76 end
73 77
74 78 def test_destroy
75 79 a = Attachment.new(:container => Issue.find(1),
76 80 :file => uploaded_test_file("testfile.txt", "text/plain"),
77 81 :author => User.find(1))
78 82 assert a.save
79 83 assert_equal 'testfile.txt', a.filename
80 84 assert_equal 59, a.filesize
81 85 assert_equal 'text/plain', a.content_type
82 86 assert_equal 0, a.downloads
83 87 assert_equal '1478adae0d4eb06d35897518540e25d6', a.digest
84 88 diskfile = a.diskfile
85 89 assert File.exist?(diskfile)
86 90 assert_equal 59, File.size(a.diskfile)
87 91 assert a.destroy
88 92 assert !File.exist?(diskfile)
89 93 end
90 94
91 95 def test_destroy_should_not_delete_file_referenced_by_other_attachment
92 96 a = Attachment.create!(:container => Issue.find(1),
93 97 :file => uploaded_test_file("testfile.txt", "text/plain"),
94 98 :author => User.find(1))
95 99 diskfile = a.diskfile
96 100
97 101 copy = a.copy
98 102 copy.save!
99 103
100 104 assert File.exists?(diskfile)
101 105 a.destroy
102 106 assert File.exists?(diskfile)
103 107 copy.destroy
104 108 assert !File.exists?(diskfile)
105 109 end
106 110
107 111 def test_create_should_auto_assign_content_type
108 112 a = Attachment.new(:container => Issue.find(1),
109 113 :file => uploaded_test_file("testfile.txt", ""),
110 114 :author => User.find(1))
111 115 assert a.save
112 116 assert_equal 'text/plain', a.content_type
113 117 end
114 118
115 119 def test_identical_attachments_at_the_same_time_should_not_overwrite
116 120 a1 = Attachment.create!(:container => Issue.find(1),
117 121 :file => uploaded_test_file("testfile.txt", ""),
118 122 :author => User.find(1))
119 123 a2 = Attachment.create!(:container => Issue.find(1),
120 124 :file => uploaded_test_file("testfile.txt", ""),
121 125 :author => User.find(1))
122 126 assert a1.disk_filename != a2.disk_filename
123 127 end
124 128
125 129 def test_filename_should_be_basenamed
126 130 a = Attachment.new(:file => MockFile.new(:original_filename => "path/to/the/file"))
127 131 assert_equal 'file', a.filename
128 132 end
129 133
130 134 def test_filename_should_be_sanitized
131 135 a = Attachment.new(:file => MockFile.new(:original_filename => "valid:[] invalid:?%*|\"'<>chars"))
132 136 assert_equal 'valid_[] invalid_chars', a.filename
133 137 end
134 138
135 139 def test_diskfilename
136 140 assert Attachment.disk_filename("test_file.txt") =~ /^\d{12}_test_file.txt$/
137 141 assert_equal 'test_file.txt', Attachment.disk_filename("test_file.txt")[13..-1]
138 142 assert_equal '770c509475505f37c2b8fb6030434d6b.txt', Attachment.disk_filename("test_accentuΓ©.txt")[13..-1]
139 143 assert_equal 'f8139524ebb8f32e51976982cd20a85d', Attachment.disk_filename("test_accentuΓ©")[13..-1]
140 144 assert_equal 'cbb5b0f30978ba03731d61f9f6d10011', Attachment.disk_filename("test_accentuΓ©.Γ§a")[13..-1]
141 145 end
142 146
143 147 context "Attachmnet.attach_files" do
144 148 should "attach the file" do
145 149 issue = Issue.first
146 150 assert_difference 'Attachment.count' do
147 151 Attachment.attach_files(issue,
148 152 '1' => {
149 153 'file' => uploaded_test_file('testfile.txt', 'text/plain'),
150 154 'description' => 'test'
151 155 })
152 156 end
153 157
154 158 attachment = Attachment.first(:order => 'id DESC')
155 159 assert_equal issue, attachment.container
156 160 assert_equal 'testfile.txt', attachment.filename
157 161 assert_equal 59, attachment.filesize
158 162 assert_equal 'test', attachment.description
159 163 assert_equal 'text/plain', attachment.content_type
160 164 assert File.exists?(attachment.diskfile)
161 165 assert_equal 59, File.size(attachment.diskfile)
162 166 end
163 167
164 168 should "add unsaved files to the object as unsaved attachments" do
165 169 # Max size of 0 to force Attachment creation failures
166 170 with_settings(:attachment_max_size => 0) do
167 171 @project = Project.generate!
168 172 response = Attachment.attach_files(@project, {
169 173 '1' => {'file' => mock_file, 'description' => 'test'},
170 174 '2' => {'file' => mock_file, 'description' => 'test'}
171 175 })
172 176
173 177 assert response[:unsaved].present?
174 178 assert_equal 2, response[:unsaved].length
175 179 assert response[:unsaved].first.new_record?
176 180 assert response[:unsaved].second.new_record?
177 181 assert_equal response[:unsaved], @project.unsaved_attachments
178 182 end
179 183 end
180 184 end
181 185
182 186 def test_latest_attach
183 187 set_fixtures_attachments_directory
184 188 a1 = Attachment.find(16)
185 189 assert_equal "testfile.png", a1.filename
186 190 assert a1.readable?
187 191 assert (! a1.visible?(User.anonymous))
188 192 assert a1.visible?(User.find(2))
189 193 a2 = Attachment.find(17)
190 194 assert_equal "testfile.PNG", a2.filename
191 195 assert a2.readable?
192 196 assert (! a2.visible?(User.anonymous))
193 197 assert a2.visible?(User.find(2))
194 198 assert a1.created_on < a2.created_on
195 199
196 200 la1 = Attachment.latest_attach([a1, a2], "testfile.png")
197 201 assert_equal 17, la1.id
198 202 la2 = Attachment.latest_attach([a1, a2], "Testfile.PNG")
199 203 assert_equal 17, la2.id
200 204
201 205 set_tmp_attachments_directory
202 206 end
203 207 end
@@ -1,60 +1,97
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 module Redmine
19 19 module Acts
20 20 module Attachable
21 21 def self.included(base)
22 22 base.extend ClassMethods
23 23 end
24 24
25 25 module ClassMethods
26 26 def acts_as_attachable(options = {})
27 27 cattr_accessor :attachable_options
28 28 self.attachable_options = {}
29 29 attachable_options[:view_permission] = options.delete(:view_permission) || "view_#{self.name.pluralize.underscore}".to_sym
30 30 attachable_options[:delete_permission] = options.delete(:delete_permission) || "edit_#{self.name.pluralize.underscore}".to_sym
31 31
32 32 has_many :attachments, options.merge(:as => :container,
33 33 :order => "#{Attachment.table_name}.created_on",
34 34 :dependent => :destroy)
35 attr_accessor :unsaved_attachments
36 35 send :include, Redmine::Acts::Attachable::InstanceMethods
36 before_save :attach_saved_attachments
37 37 end
38 38 end
39 39
40 40 module InstanceMethods
41 41 def self.included(base)
42 42 base.extend ClassMethods
43 43 end
44 44
45 45 def attachments_visible?(user=User.current)
46 46 (respond_to?(:visible?) ? visible?(user) : true) &&
47 47 user.allowed_to?(self.class.attachable_options[:view_permission], self.project)
48 48 end
49 49
50 50 def attachments_deletable?(user=User.current)
51 51 (respond_to?(:visible?) ? visible?(user) : true) &&
52 52 user.allowed_to?(self.class.attachable_options[:delete_permission], self.project)
53 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 92 module ClassMethods
56 93 end
57 94 end
58 95 end
59 96 end
60 97 end
General Comments 0
You need to be logged in to leave comments. Login now