##// END OF EJS Templates
Ability to sort issues by grouped column (#3511)....
Jean-Philippe Lang -
r10543:9f148e098b07
parent child
Show More
@@ -1,442 +1,443
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 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, :destroy]
24 24 before_filter :find_project, :only => [:new, :create]
25 25 before_filter :authorize, :except => [:index]
26 26 before_filter :find_optional_project, :only => [:index]
27 27 before_filter :check_for_default_issue_status, :only => [:new, :create]
28 28 before_filter :build_new_issue_from_params, :only => [:new, :create]
29 29 accept_rss_auth :index, :show
30 30 accept_api_auth :index, :show, :create, :update, :destroy
31 31
32 32 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
33 33
34 34 helper :journals
35 35 helper :projects
36 36 include ProjectsHelper
37 37 helper :custom_fields
38 38 include CustomFieldsHelper
39 39 helper :issue_relations
40 40 include IssueRelationsHelper
41 41 helper :watchers
42 42 include WatchersHelper
43 43 helper :attachments
44 44 include AttachmentsHelper
45 45 helper :queries
46 46 include QueriesHelper
47 47 helper :repositories
48 48 include RepositoriesHelper
49 49 helper :sort
50 50 include SortHelper
51 51 include IssuesHelper
52 52 helper :timelog
53 53 include Redmine::Export::PDF
54 54
55 55 def index
56 56 retrieve_query
57 57 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
58 58 sort_update(@query.sortable_columns)
59 @query.sort_criteria = sort_criteria.to_a
59 60
60 61 if @query.valid?
61 62 case params[:format]
62 63 when 'csv', 'pdf'
63 64 @limit = Setting.issues_export_limit.to_i
64 65 when 'atom'
65 66 @limit = Setting.feeds_limit.to_i
66 67 when 'xml', 'json'
67 68 @offset, @limit = api_offset_and_limit
68 69 else
69 70 @limit = per_page_option
70 71 end
71 72
72 73 @issue_count = @query.issue_count
73 74 @issue_pages = Paginator.new self, @issue_count, @limit, params['page']
74 75 @offset ||= @issue_pages.current.offset
75 76 @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
76 77 :order => sort_clause,
77 78 :offset => @offset,
78 79 :limit => @limit)
79 80 @issue_count_by_group = @query.issue_count_by_group
80 81
81 82 respond_to do |format|
82 83 format.html { render :template => 'issues/index', :layout => !request.xhr? }
83 84 format.api {
84 85 Issue.load_visible_relations(@issues) if include_in_api_response?('relations')
85 86 }
86 87 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
87 88 format.csv { send_data(issues_to_csv(@issues, @project, @query, params), :type => 'text/csv; header=present', :filename => 'export.csv') }
88 89 format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'export.pdf') }
89 90 end
90 91 else
91 92 respond_to do |format|
92 93 format.html { render(:template => 'issues/index', :layout => !request.xhr?) }
93 94 format.any(:atom, :csv, :pdf) { render(:nothing => true) }
94 95 format.api { render_validation_errors(@query) }
95 96 end
96 97 end
97 98 rescue ActiveRecord::RecordNotFound
98 99 render_404
99 100 end
100 101
101 102 def show
102 103 @journals = @issue.journals.includes(:user, :details).reorder("#{Journal.table_name}.id ASC").all
103 104 @journals.each_with_index {|j,i| j.indice = i+1}
104 105 @journals.reject!(&:private_notes?) unless User.current.allowed_to?(:view_private_notes, @issue.project)
105 106 @journals.reverse! if User.current.wants_comments_in_reverse_order?
106 107
107 108 @changesets = @issue.changesets.visible.all
108 109 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
109 110
110 111 @relations = @issue.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
111 112 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
112 113 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
113 114 @priorities = IssuePriority.active
114 115 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
115 116 respond_to do |format|
116 117 format.html {
117 118 retrieve_previous_and_next_issue_ids
118 119 render :template => 'issues/show'
119 120 }
120 121 format.api
121 122 format.atom { render :template => 'journals/index', :layout => false, :content_type => 'application/atom+xml' }
122 123 format.pdf {
123 124 pdf = issue_to_pdf(@issue, :journals => @journals)
124 125 send_data(pdf, :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf")
125 126 }
126 127 end
127 128 end
128 129
129 130 # Add a new issue
130 131 # The new issue will be created from an existing one if copy_from parameter is given
131 132 def new
132 133 respond_to do |format|
133 134 format.html { render :action => 'new', :layout => !request.xhr? }
134 135 format.js { render :partial => 'update_form' }
135 136 end
136 137 end
137 138
138 139 def create
139 140 call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
140 141 @issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads]))
141 142 if @issue.save
142 143 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
143 144 respond_to do |format|
144 145 format.html {
145 146 render_attachment_warning_if_needed(@issue)
146 147 flash[:notice] = l(:notice_issue_successful_create, :id => view_context.link_to("##{@issue.id}", issue_path(@issue), :title => @issue.subject))
147 148 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?} } :
148 149 { :action => 'show', :id => @issue })
149 150 }
150 151 format.api { render :action => 'show', :status => :created, :location => issue_url(@issue) }
151 152 end
152 153 return
153 154 else
154 155 respond_to do |format|
155 156 format.html { render :action => 'new' }
156 157 format.api { render_validation_errors(@issue) }
157 158 end
158 159 end
159 160 end
160 161
161 162 def edit
162 163 return unless update_issue_from_params
163 164
164 165 respond_to do |format|
165 166 format.html { }
166 167 format.xml { }
167 168 end
168 169 end
169 170
170 171 def update
171 172 return unless update_issue_from_params
172 173 @issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads]))
173 174 saved = false
174 175 begin
175 176 saved = @issue.save_issue_with_child_records(params, @time_entry)
176 177 rescue ActiveRecord::StaleObjectError
177 178 @conflict = true
178 179 if params[:last_journal_id]
179 180 @conflict_journals = @issue.journals_after(params[:last_journal_id]).all
180 181 @conflict_journals.reject!(&:private_notes?) unless User.current.allowed_to?(:view_private_notes, @issue.project)
181 182 end
182 183 end
183 184
184 185 if saved
185 186 render_attachment_warning_if_needed(@issue)
186 187 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
187 188
188 189 respond_to do |format|
189 190 format.html { redirect_back_or_default({:action => 'show', :id => @issue}) }
190 191 format.api { render_api_ok }
191 192 end
192 193 else
193 194 respond_to do |format|
194 195 format.html { render :action => 'edit' }
195 196 format.api { render_validation_errors(@issue) }
196 197 end
197 198 end
198 199 end
199 200
200 201 # Bulk edit/copy a set of issues
201 202 def bulk_edit
202 203 @issues.sort!
203 204 @copy = params[:copy].present?
204 205 @notes = params[:notes]
205 206
206 207 if User.current.allowed_to?(:move_issues, @projects)
207 208 @allowed_projects = Issue.allowed_target_projects_on_move
208 209 if params[:issue]
209 210 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:issue][:project_id].to_s}
210 211 if @target_project
211 212 target_projects = [@target_project]
212 213 end
213 214 end
214 215 end
215 216 target_projects ||= @projects
216 217
217 218 if @copy
218 219 @available_statuses = [IssueStatus.default]
219 220 else
220 221 @available_statuses = @issues.map(&:new_statuses_allowed_to).reduce(:&)
221 222 end
222 223 @custom_fields = target_projects.map{|p|p.all_issue_custom_fields}.reduce(:&)
223 224 @assignables = target_projects.map(&:assignable_users).reduce(:&)
224 225 @trackers = target_projects.map(&:trackers).reduce(:&)
225 226 @versions = target_projects.map {|p| p.shared_versions.open}.reduce(:&)
226 227 @categories = target_projects.map {|p| p.issue_categories}.reduce(:&)
227 228 if @copy
228 229 @attachments_present = @issues.detect {|i| i.attachments.any?}.present?
229 230 @subtasks_present = @issues.detect {|i| !i.leaf?}.present?
230 231 end
231 232
232 233 @safe_attributes = @issues.map(&:safe_attribute_names).reduce(:&)
233 234 render :layout => false if request.xhr?
234 235 end
235 236
236 237 def bulk_update
237 238 @issues.sort!
238 239 @copy = params[:copy].present?
239 240 attributes = parse_params_for_bulk_issue_attributes(params)
240 241
241 242 unsaved_issue_ids = []
242 243 moved_issues = []
243 244
244 245 if @copy && params[:copy_subtasks].present?
245 246 # Descendant issues will be copied with the parent task
246 247 # Don't copy them twice
247 248 @issues.reject! {|issue| @issues.detect {|other| issue.is_descendant_of?(other)}}
248 249 end
249 250
250 251 @issues.each do |issue|
251 252 issue.reload
252 253 if @copy
253 254 issue = issue.copy({},
254 255 :attachments => params[:copy_attachments].present?,
255 256 :subtasks => params[:copy_subtasks].present?
256 257 )
257 258 end
258 259 journal = issue.init_journal(User.current, params[:notes])
259 260 issue.safe_attributes = attributes
260 261 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
261 262 if issue.save
262 263 moved_issues << issue
263 264 else
264 265 # Keep unsaved issue ids to display them in flash error
265 266 unsaved_issue_ids << issue.id
266 267 end
267 268 end
268 269 set_flash_from_bulk_issue_save(@issues, unsaved_issue_ids)
269 270
270 271 if params[:follow]
271 272 if @issues.size == 1 && moved_issues.size == 1
272 273 redirect_to :controller => 'issues', :action => 'show', :id => moved_issues.first
273 274 elsif moved_issues.map(&:project).uniq.size == 1
274 275 redirect_to :controller => 'issues', :action => 'index', :project_id => moved_issues.map(&:project).first
275 276 end
276 277 else
277 278 redirect_back_or_default({:controller => 'issues', :action => 'index', :project_id => @project})
278 279 end
279 280 end
280 281
281 282 def destroy
282 283 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
283 284 if @hours > 0
284 285 case params[:todo]
285 286 when 'destroy'
286 287 # nothing to do
287 288 when 'nullify'
288 289 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
289 290 when 'reassign'
290 291 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
291 292 if reassign_to.nil?
292 293 flash.now[:error] = l(:error_issue_not_found_in_project)
293 294 return
294 295 else
295 296 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
296 297 end
297 298 else
298 299 # display the destroy form if it's a user request
299 300 return unless api_request?
300 301 end
301 302 end
302 303 @issues.each do |issue|
303 304 begin
304 305 issue.reload.destroy
305 306 rescue ::ActiveRecord::RecordNotFound # raised by #reload if issue no longer exists
306 307 # nothing to do, issue was already deleted (eg. by a parent)
307 308 end
308 309 end
309 310 respond_to do |format|
310 311 format.html { redirect_back_or_default(:action => 'index', :project_id => @project) }
311 312 format.api { render_api_ok }
312 313 end
313 314 end
314 315
315 316 private
316 317 def find_issue
317 318 # Issue.visible.find(...) can not be used to redirect user to the login form
318 319 # if the issue actually exists but requires authentication
319 320 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
320 321 unless @issue.visible?
321 322 deny_access
322 323 return
323 324 end
324 325 @project = @issue.project
325 326 rescue ActiveRecord::RecordNotFound
326 327 render_404
327 328 end
328 329
329 330 def find_project
330 331 project_id = params[:project_id] || (params[:issue] && params[:issue][:project_id])
331 332 @project = Project.find(project_id)
332 333 rescue ActiveRecord::RecordNotFound
333 334 render_404
334 335 end
335 336
336 337 def retrieve_previous_and_next_issue_ids
337 338 retrieve_query_from_session
338 339 if @query
339 340 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
340 341 sort_update(@query.sortable_columns, 'issues_index_sort')
341 342 limit = 500
342 343 issue_ids = @query.issue_ids(:order => sort_clause, :limit => (limit + 1), :include => [:assigned_to, :tracker, :priority, :category, :fixed_version])
343 344 if (idx = issue_ids.index(@issue.id)) && idx < limit
344 345 if issue_ids.size < 500
345 346 @issue_position = idx + 1
346 347 @issue_count = issue_ids.size
347 348 end
348 349 @prev_issue_id = issue_ids[idx - 1] if idx > 0
349 350 @next_issue_id = issue_ids[idx + 1] if idx < (issue_ids.size - 1)
350 351 end
351 352 end
352 353 end
353 354
354 355 # Used by #edit and #update to set some common instance variables
355 356 # from the params
356 357 # TODO: Refactor, not everything in here is needed by #edit
357 358 def update_issue_from_params
358 359 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
359 360 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
360 361 @time_entry.attributes = params[:time_entry]
361 362
362 363 @issue.init_journal(User.current)
363 364
364 365 issue_attributes = params[:issue]
365 366 if issue_attributes && params[:conflict_resolution]
366 367 case params[:conflict_resolution]
367 368 when 'overwrite'
368 369 issue_attributes = issue_attributes.dup
369 370 issue_attributes.delete(:lock_version)
370 371 when 'add_notes'
371 372 issue_attributes = issue_attributes.slice(:notes)
372 373 when 'cancel'
373 374 redirect_to issue_path(@issue)
374 375 return false
375 376 end
376 377 end
377 378 @issue.safe_attributes = issue_attributes
378 379 @priorities = IssuePriority.active
379 380 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
380 381 true
381 382 end
382 383
383 384 # TODO: Refactor, lots of extra code in here
384 385 # TODO: Changing tracker on an existing issue should not trigger this
385 386 def build_new_issue_from_params
386 387 if params[:id].blank?
387 388 @issue = Issue.new
388 389 if params[:copy_from]
389 390 begin
390 391 @copy_from = Issue.visible.find(params[:copy_from])
391 392 @copy_attachments = params[:copy_attachments].present? || request.get?
392 393 @copy_subtasks = params[:copy_subtasks].present? || request.get?
393 394 @issue.copy_from(@copy_from, :attachments => @copy_attachments, :subtasks => @copy_subtasks)
394 395 rescue ActiveRecord::RecordNotFound
395 396 render_404
396 397 return
397 398 end
398 399 end
399 400 @issue.project = @project
400 401 else
401 402 @issue = @project.issues.visible.find(params[:id])
402 403 end
403 404
404 405 @issue.project = @project
405 406 @issue.author ||= User.current
406 407 # Tracker must be set before custom field values
407 408 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
408 409 if @issue.tracker.nil?
409 410 render_error l(:error_no_tracker_in_project)
410 411 return false
411 412 end
412 413 @issue.start_date ||= Date.today if Setting.default_issue_start_date_to_creation_date?
413 414 @issue.safe_attributes = params[:issue]
414 415
415 416 @priorities = IssuePriority.active
416 417 @allowed_statuses = @issue.new_statuses_allowed_to(User.current, true)
417 418 @available_watchers = (@issue.project.users.sort + @issue.watcher_users).uniq
418 419 end
419 420
420 421 def check_for_default_issue_status
421 422 if IssueStatus.default.nil?
422 423 render_error l(:error_no_default_issue_status)
423 424 return false
424 425 end
425 426 end
426 427
427 428 def parse_params_for_bulk_issue_attributes(params)
428 429 attributes = (params[:issue] || {}).reject {|k,v| v.blank?}
429 430 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
430 431 if custom = attributes[:custom_field_values]
431 432 custom.reject! {|k,v| v.blank?}
432 433 custom.keys.each do |k|
433 434 if custom[k].is_a?(Array)
434 435 custom[k] << '' if custom[k].delete('__none__')
435 436 else
436 437 custom[k] = '' if custom[k] == '__none__'
437 438 end
438 439 end
439 440 end
440 441 attributes
441 442 end
442 443 end
@@ -1,234 +1,242
1 1 # encoding: utf-8
2 2 #
3 3 # Helpers to sort tables using clickable column headers.
4 4 #
5 5 # Author: Stuart Rackham <srackham@methods.co.nz>, March 2005.
6 6 # Jean-Philippe Lang, 2009
7 7 # License: This source code is released under the MIT license.
8 8 #
9 9 # - Consecutive clicks toggle the column's sort order.
10 10 # - Sort state is maintained by a session hash entry.
11 11 # - CSS classes identify sort column and state.
12 12 # - Typically used in conjunction with the Pagination module.
13 13 #
14 14 # Example code snippets:
15 15 #
16 16 # Controller:
17 17 #
18 18 # helper :sort
19 19 # include SortHelper
20 20 #
21 21 # def list
22 22 # sort_init 'last_name'
23 23 # sort_update %w(first_name last_name)
24 24 # @items = Contact.find_all nil, sort_clause
25 25 # end
26 26 #
27 27 # Controller (using Pagination module):
28 28 #
29 29 # helper :sort
30 30 # include SortHelper
31 31 #
32 32 # def list
33 33 # sort_init 'last_name'
34 34 # sort_update %w(first_name last_name)
35 35 # @contact_pages, @items = paginate :contacts,
36 36 # :order_by => sort_clause,
37 37 # :per_page => 10
38 38 # end
39 39 #
40 40 # View (table header in list.rhtml):
41 41 #
42 42 # <thead>
43 43 # <tr>
44 44 # <%= sort_header_tag('id', :title => 'Sort by contact ID') %>
45 45 # <%= sort_header_tag('last_name', :caption => 'Name') %>
46 46 # <%= sort_header_tag('phone') %>
47 47 # <%= sort_header_tag('address', :width => 200) %>
48 48 # </tr>
49 49 # </thead>
50 50 #
51 51 # - Introduces instance variables: @sort_default, @sort_criteria
52 52 # - Introduces param :sort
53 53 #
54 54
55 55 module SortHelper
56 56 class SortCriteria
57 57
58 58 def initialize
59 59 @criteria = []
60 60 end
61 61
62 62 def available_criteria=(criteria)
63 63 unless criteria.is_a?(Hash)
64 64 criteria = criteria.inject({}) {|h,k| h[k] = k; h}
65 65 end
66 66 @available_criteria = criteria
67 67 end
68 68
69 69 def from_param(param)
70 70 @criteria = param.to_s.split(',').collect {|s| s.split(':')[0..1]}
71 71 normalize!
72 72 end
73 73
74 74 def criteria=(arg)
75 75 @criteria = arg
76 76 normalize!
77 77 end
78 78
79 79 def to_param
80 80 @criteria.collect {|k,o| k + (o ? '' : ':desc')}.join(',')
81 81 end
82 82
83 83 def to_sql
84 84 sql = @criteria.collect do |k,o|
85 85 if s = @available_criteria[k]
86 86 (o ? s.to_a : s.to_a.collect {|c| append_desc(c)}).join(', ')
87 87 end
88 88 end.compact.join(', ')
89 89 sql.blank? ? nil : sql
90 90 end
91 91
92 def to_a
93 @criteria.dup
94 end
95
92 96 def add!(key, asc)
93 97 @criteria.delete_if {|k,o| k == key}
94 98 @criteria = [[key, asc]] + @criteria
95 99 normalize!
96 100 end
97 101
98 102 def add(*args)
99 103 r = self.class.new.from_param(to_param)
100 104 r.add!(*args)
101 105 r
102 106 end
103 107
104 108 def first_key
105 109 @criteria.first && @criteria.first.first
106 110 end
107 111
108 112 def first_asc?
109 113 @criteria.first && @criteria.first.last
110 114 end
111 115
112 116 def empty?
113 117 @criteria.empty?
114 118 end
115 119
116 120 private
117 121
118 122 def normalize!
119 123 @criteria ||= []
120 124 @criteria = @criteria.collect {|s| s = s.to_a; [s.first, (s.last == false || s.last == 'desc') ? false : true]}
121 125 @criteria = @criteria.select {|k,o| @available_criteria.has_key?(k)} if @available_criteria
122 126 @criteria.slice!(3)
123 127 self
124 128 end
125 129
126 130 # Appends DESC to the sort criterion unless it has a fixed order
127 131 def append_desc(criterion)
128 132 if criterion =~ / (asc|desc)$/i
129 133 criterion
130 134 else
131 135 "#{criterion} DESC"
132 136 end
133 137 end
134 138 end
135 139
136 140 def sort_name
137 141 controller_name + '_' + action_name + '_sort'
138 142 end
139 143
140 144 # Initializes the default sort.
141 145 # Examples:
142 146 #
143 147 # sort_init 'name'
144 148 # sort_init 'id', 'desc'
145 149 # sort_init ['name', ['id', 'desc']]
146 150 # sort_init [['name', 'desc'], ['id', 'desc']]
147 151 #
148 152 def sort_init(*args)
149 153 case args.size
150 154 when 1
151 155 @sort_default = args.first.is_a?(Array) ? args.first : [[args.first]]
152 156 when 2
153 157 @sort_default = [[args.first, args.last]]
154 158 else
155 159 raise ArgumentError
156 160 end
157 161 end
158 162
159 163 # Updates the sort state. Call this in the controller prior to calling
160 164 # sort_clause.
161 165 # - criteria can be either an array or a hash of allowed keys
162 166 #
163 167 def sort_update(criteria, sort_name=nil)
164 168 sort_name ||= self.sort_name
165 169 @sort_criteria = SortCriteria.new
166 170 @sort_criteria.available_criteria = criteria
167 171 @sort_criteria.from_param(params[:sort] || session[sort_name])
168 172 @sort_criteria.criteria = @sort_default if @sort_criteria.empty?
169 173 session[sort_name] = @sort_criteria.to_param
170 174 end
171 175
172 176 # Clears the sort criteria session data
173 177 #
174 178 def sort_clear
175 179 session[sort_name] = nil
176 180 end
177 181
178 182 # Returns an SQL sort clause corresponding to the current sort state.
179 183 # Use this to sort the controller's table items collection.
180 184 #
181 185 def sort_clause()
182 186 @sort_criteria.to_sql
183 187 end
184 188
189 def sort_criteria
190 @sort_criteria
191 end
192
185 193 # Returns a link which sorts by the named column.
186 194 #
187 195 # - column is the name of an attribute in the sorted record collection.
188 196 # - the optional caption explicitly specifies the displayed link text.
189 197 # - 2 CSS classes reflect the state of the link: sort and asc or desc
190 198 #
191 199 def sort_link(column, caption, default_order)
192 200 css, order = nil, default_order
193 201
194 202 if column.to_s == @sort_criteria.first_key
195 203 if @sort_criteria.first_asc?
196 204 css = 'sort asc'
197 205 order = 'desc'
198 206 else
199 207 css = 'sort desc'
200 208 order = 'asc'
201 209 end
202 210 end
203 211 caption = column.to_s.humanize unless caption
204 212
205 213 sort_options = { :sort => @sort_criteria.add(column.to_s, order).to_param }
206 214 url_options = params.merge(sort_options)
207 215
208 216 # Add project_id to url_options
209 217 url_options = url_options.merge(:project_id => params[:project_id]) if params.has_key?(:project_id)
210 218
211 219 link_to_content_update(h(caption), url_options, :class => css)
212 220 end
213 221
214 222 # Returns a table header <th> tag with a sort link for the named column
215 223 # attribute.
216 224 #
217 225 # Options:
218 226 # :caption The displayed link name (defaults to titleized column name).
219 227 # :title The tag's 'title' attribute (defaults to 'Sort by :caption').
220 228 #
221 229 # Other options hash entries generate additional table header tag attributes.
222 230 #
223 231 # Example:
224 232 #
225 233 # <%= sort_header_tag('id', :title => 'Sort by contact ID', :width => 40) %>
226 234 #
227 235 def sort_header_tag(column, options = {})
228 236 caption = options.delete(:caption) || column.to_s.humanize
229 237 default_order = options.delete(:default_order) || 'asc'
230 238 options[:title] = l(:label_sort_by, "\"#{caption}\"") unless options[:title]
231 239 content_tag('th', sort_link(column, caption, default_order), options)
232 240 end
233 241 end
234 242
@@ -1,1063 +1,1068
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 class QueryColumn
19 19 attr_accessor :name, :sortable, :groupable, :default_order
20 20 include Redmine::I18n
21 21
22 22 def initialize(name, options={})
23 23 self.name = name
24 24 self.sortable = options[:sortable]
25 25 self.groupable = options[:groupable] || false
26 26 if groupable == true
27 27 self.groupable = name.to_s
28 28 end
29 29 self.default_order = options[:default_order]
30 30 @caption_key = options[:caption] || "field_#{name}"
31 31 end
32 32
33 33 def caption
34 34 l(@caption_key)
35 35 end
36 36
37 37 # Returns true if the column is sortable, otherwise false
38 38 def sortable?
39 39 !@sortable.nil?
40 40 end
41 41
42 42 def sortable
43 43 @sortable.is_a?(Proc) ? @sortable.call : @sortable
44 44 end
45 45
46 46 def value(issue)
47 47 issue.send name
48 48 end
49 49
50 50 def css_classes
51 51 name
52 52 end
53 53 end
54 54
55 55 class QueryCustomFieldColumn < QueryColumn
56 56
57 57 def initialize(custom_field)
58 58 self.name = "cf_#{custom_field.id}".to_sym
59 59 self.sortable = custom_field.order_statement || false
60 60 self.groupable = custom_field.group_statement || false
61 61 @cf = custom_field
62 62 end
63 63
64 64 def caption
65 65 @cf.name
66 66 end
67 67
68 68 def custom_field
69 69 @cf
70 70 end
71 71
72 72 def value(issue)
73 73 cv = issue.custom_values.select {|v| v.custom_field_id == @cf.id}.collect {|v| @cf.cast_value(v.value)}
74 74 cv.size > 1 ? cv.sort {|a,b| a.to_s <=> b.to_s} : cv.first
75 75 end
76 76
77 77 def css_classes
78 78 @css_classes ||= "#{name} #{@cf.field_format}"
79 79 end
80 80 end
81 81
82 82 class Query < ActiveRecord::Base
83 83 class StatementInvalid < ::ActiveRecord::StatementInvalid
84 84 end
85 85
86 86 belongs_to :project
87 87 belongs_to :user
88 88 serialize :filters
89 89 serialize :column_names
90 90 serialize :sort_criteria, Array
91 91
92 92 attr_protected :project_id, :user_id
93 93
94 94 validates_presence_of :name
95 95 validates_length_of :name, :maximum => 255
96 96 validate :validate_query_filters
97 97
98 98 @@operators = { "=" => :label_equals,
99 99 "!" => :label_not_equals,
100 100 "o" => :label_open_issues,
101 101 "c" => :label_closed_issues,
102 102 "!*" => :label_none,
103 103 "*" => :label_any,
104 104 ">=" => :label_greater_or_equal,
105 105 "<=" => :label_less_or_equal,
106 106 "><" => :label_between,
107 107 "<t+" => :label_in_less_than,
108 108 ">t+" => :label_in_more_than,
109 109 "t+" => :label_in,
110 110 "t" => :label_today,
111 111 "w" => :label_this_week,
112 112 ">t-" => :label_less_than_ago,
113 113 "<t-" => :label_more_than_ago,
114 114 "t-" => :label_ago,
115 115 "~" => :label_contains,
116 116 "!~" => :label_not_contains,
117 117 "=p" => :label_any_issues_in_project,
118 118 "=!p" => :label_any_issues_not_in_project,
119 119 "!p" => :label_no_issues_in_project}
120 120
121 121 cattr_reader :operators
122 122
123 123 @@operators_by_filter_type = { :list => [ "=", "!" ],
124 124 :list_status => [ "o", "=", "!", "c", "*" ],
125 125 :list_optional => [ "=", "!", "!*", "*" ],
126 126 :list_subprojects => [ "*", "!*", "=" ],
127 127 :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-", "!*", "*" ],
128 128 :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "t-", "t", "w", "!*", "*" ],
129 129 :string => [ "=", "~", "!", "!~", "!*", "*" ],
130 130 :text => [ "~", "!~", "!*", "*" ],
131 131 :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
132 132 :float => [ "=", ">=", "<=", "><", "!*", "*" ],
133 133 :relation => ["=", "=p", "=!p", "!p", "!*", "*"]}
134 134
135 135 cattr_reader :operators_by_filter_type
136 136
137 137 @@available_columns = [
138 138 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
139 139 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
140 140 QueryColumn.new(:parent, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc', :caption => :field_parent_issue),
141 141 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
142 142 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
143 143 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
144 144 QueryColumn.new(:author, :sortable => lambda {User.fields_for_order_statement("authors")}, :groupable => true),
145 145 QueryColumn.new(:assigned_to, :sortable => lambda {User.fields_for_order_statement}, :groupable => true),
146 146 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
147 147 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
148 148 QueryColumn.new(:fixed_version, :sortable => lambda {Version.fields_for_order_statement}, :groupable => true),
149 149 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
150 150 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
151 151 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
152 152 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
153 153 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
154 154 QueryColumn.new(:relations, :caption => :label_related_issues)
155 155 ]
156 156 cattr_reader :available_columns
157 157
158 158 scope :visible, lambda {|*args|
159 159 user = args.shift || User.current
160 160 base = Project.allowed_to_condition(user, :view_issues, *args)
161 161 user_id = user.logged? ? user.id : 0
162 162 {
163 163 :conditions => ["(#{table_name}.project_id IS NULL OR (#{base})) AND (#{table_name}.is_public = ? OR #{table_name}.user_id = ?)", true, user_id],
164 164 :include => :project
165 165 }
166 166 }
167 167
168 168 def initialize(attributes=nil, *args)
169 169 super attributes
170 170 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
171 171 @is_for_all = project.nil?
172 172 end
173 173
174 174 def validate_query_filters
175 175 filters.each_key do |field|
176 176 if values_for(field)
177 177 case type_for(field)
178 178 when :integer
179 179 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+$/) }
180 180 when :float
181 181 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+(\.\d*)?$/) }
182 182 when :date, :date_past
183 183 case operator_for(field)
184 184 when "=", ">=", "<=", "><"
185 185 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && (!v.match(/^\d{4}-\d{2}-\d{2}$/) || (Date.parse(v) rescue nil).nil?) }
186 186 when ">t-", "<t-", "t-"
187 187 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
188 188 end
189 189 end
190 190 end
191 191
192 192 add_filter_error(field, :blank) unless
193 193 # filter requires one or more values
194 194 (values_for(field) and !values_for(field).first.blank?) or
195 195 # filter doesn't require any value
196 196 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
197 197 end if filters
198 198 end
199 199
200 200 def add_filter_error(field, message)
201 201 m = label_for(field) + " " + l(message, :scope => 'activerecord.errors.messages')
202 202 errors.add(:base, m)
203 203 end
204 204
205 205 # Returns true if the query is visible to +user+ or the current user.
206 206 def visible?(user=User.current)
207 207 (project.nil? || user.allowed_to?(:view_issues, project)) && (self.is_public? || self.user_id == user.id)
208 208 end
209 209
210 210 def editable_by?(user)
211 211 return false unless user
212 212 # Admin can edit them all and regular users can edit their private queries
213 213 return true if user.admin? || (!is_public && self.user_id == user.id)
214 214 # Members can not edit public queries that are for all project (only admin is allowed to)
215 215 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
216 216 end
217 217
218 218 def trackers
219 219 @trackers ||= project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
220 220 end
221 221
222 222 # Returns a hash of localized labels for all filter operators
223 223 def self.operators_labels
224 224 operators.inject({}) {|h, operator| h[operator.first] = l(operator.last); h}
225 225 end
226 226
227 227 def available_filters
228 228 return @available_filters if @available_filters
229 229 @available_filters = {
230 230 "status_id" => {
231 231 :type => :list_status, :order => 0,
232 232 :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] }
233 233 },
234 234 "tracker_id" => {
235 235 :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] }
236 236 },
237 237 "priority_id" => {
238 238 :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] }
239 239 },
240 240 "subject" => { :type => :text, :order => 8 },
241 241 "created_on" => { :type => :date_past, :order => 9 },
242 242 "updated_on" => { :type => :date_past, :order => 10 },
243 243 "start_date" => { :type => :date, :order => 11 },
244 244 "due_date" => { :type => :date, :order => 12 },
245 245 "estimated_hours" => { :type => :float, :order => 13 },
246 246 "done_ratio" => { :type => :integer, :order => 14 }
247 247 }
248 248 IssueRelation::TYPES.each do |relation_type, options|
249 249 @available_filters[relation_type] = {
250 250 :type => :relation, :order => @available_filters.size + 100,
251 251 :label => options[:name]
252 252 }
253 253 end
254 254 principals = []
255 255 if project
256 256 principals += project.principals.sort
257 257 unless project.leaf?
258 258 subprojects = project.descendants.visible.all
259 259 if subprojects.any?
260 260 @available_filters["subproject_id"] = {
261 261 :type => :list_subprojects, :order => 13,
262 262 :values => subprojects.collect{|s| [s.name, s.id.to_s] }
263 263 }
264 264 principals += Principal.member_of(subprojects)
265 265 end
266 266 end
267 267 else
268 268 if all_projects.any?
269 269 # members of visible projects
270 270 principals += Principal.member_of(all_projects)
271 271 # project filter
272 272 project_values = []
273 273 if User.current.logged? && User.current.memberships.any?
274 274 project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"]
275 275 end
276 276 project_values += all_projects_values
277 277 @available_filters["project_id"] = {
278 278 :type => :list, :order => 1, :values => project_values
279 279 } unless project_values.empty?
280 280 end
281 281 end
282 282 principals.uniq!
283 283 principals.sort!
284 284 users = principals.select {|p| p.is_a?(User)}
285 285
286 286 assigned_to_values = []
287 287 assigned_to_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
288 288 assigned_to_values += (Setting.issue_group_assignment? ?
289 289 principals : users).collect{|s| [s.name, s.id.to_s] }
290 290 @available_filters["assigned_to_id"] = {
291 291 :type => :list_optional, :order => 4, :values => assigned_to_values
292 292 } unless assigned_to_values.empty?
293 293
294 294 author_values = []
295 295 author_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
296 296 author_values += users.collect{|s| [s.name, s.id.to_s] }
297 297 @available_filters["author_id"] = {
298 298 :type => :list, :order => 5, :values => author_values
299 299 } unless author_values.empty?
300 300
301 301 group_values = Group.all.collect {|g| [g.name, g.id.to_s] }
302 302 @available_filters["member_of_group"] = {
303 303 :type => :list_optional, :order => 6, :values => group_values
304 304 } unless group_values.empty?
305 305
306 306 role_values = Role.givable.collect {|r| [r.name, r.id.to_s] }
307 307 @available_filters["assigned_to_role"] = {
308 308 :type => :list_optional, :order => 7, :values => role_values
309 309 } unless role_values.empty?
310 310
311 311 if User.current.logged?
312 312 @available_filters["watcher_id"] = {
313 313 :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]]
314 314 }
315 315 end
316 316
317 317 if project
318 318 # project specific filters
319 319 categories = project.issue_categories.all
320 320 unless categories.empty?
321 321 @available_filters["category_id"] = {
322 322 :type => :list_optional, :order => 6,
323 323 :values => categories.collect{|s| [s.name, s.id.to_s] }
324 324 }
325 325 end
326 326 versions = project.shared_versions.all
327 327 unless versions.empty?
328 328 @available_filters["fixed_version_id"] = {
329 329 :type => :list_optional, :order => 7,
330 330 :values => versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] }
331 331 }
332 332 end
333 333 add_custom_fields_filters(project.all_issue_custom_fields)
334 334 else
335 335 # global filters for cross project issue list
336 336 system_shared_versions = Version.visible.find_all_by_sharing('system')
337 337 unless system_shared_versions.empty?
338 338 @available_filters["fixed_version_id"] = {
339 339 :type => :list_optional, :order => 7,
340 340 :values => system_shared_versions.sort.collect{|s|
341 341 ["#{s.project.name} - #{s.name}", s.id.to_s]
342 342 }
343 343 }
344 344 end
345 345 add_custom_fields_filters(
346 346 IssueCustomField.find(:all,
347 347 :conditions => {
348 348 :is_filter => true,
349 349 :is_for_all => true
350 350 }))
351 351 end
352 352 add_associations_custom_fields_filters :project, :author, :assigned_to, :fixed_version
353 353 if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
354 354 User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
355 355 @available_filters["is_private"] = {
356 356 :type => :list, :order => 16,
357 357 :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]]
358 358 }
359 359 end
360 360 Tracker.disabled_core_fields(trackers).each {|field|
361 361 @available_filters.delete field
362 362 }
363 363 @available_filters.each do |field, options|
364 364 options[:name] ||= l(options[:label] || "field_#{field}".gsub(/_id$/, ''))
365 365 end
366 366 @available_filters
367 367 end
368 368
369 369 # Returns a representation of the available filters for JSON serialization
370 370 def available_filters_as_json
371 371 json = {}
372 372 available_filters.each do |field, options|
373 373 json[field] = options.slice(:type, :name, :values).stringify_keys
374 374 end
375 375 json
376 376 end
377 377
378 378 def all_projects
379 379 @all_projects ||= Project.visible.all
380 380 end
381 381
382 382 def all_projects_values
383 383 return @all_projects_values if @all_projects_values
384 384
385 385 values = []
386 386 Project.project_tree(all_projects) do |p, level|
387 387 prefix = (level > 0 ? ('--' * level + ' ') : '')
388 388 values << ["#{prefix}#{p.name}", p.id.to_s]
389 389 end
390 390 @all_projects_values = values
391 391 end
392 392
393 393 def add_filter(field, operator, values)
394 394 # values must be an array
395 395 return unless values.nil? || values.is_a?(Array)
396 396 # check if field is defined as an available filter
397 397 if available_filters.has_key? field
398 398 filter_options = available_filters[field]
399 399 # check if operator is allowed for that filter
400 400 #if @@operators_by_filter_type[filter_options[:type]].include? operator
401 401 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
402 402 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
403 403 #end
404 404 filters[field] = {:operator => operator, :values => (values || [''])}
405 405 end
406 406 end
407 407
408 408 def add_short_filter(field, expression)
409 409 return unless expression && available_filters.has_key?(field)
410 410 field_type = available_filters[field][:type]
411 411 @@operators_by_filter_type[field_type].sort.reverse.detect do |operator|
412 412 next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/
413 413 add_filter field, operator, $1.present? ? $1.split('|') : ['']
414 414 end || add_filter(field, '=', expression.split('|'))
415 415 end
416 416
417 417 # Add multiple filters using +add_filter+
418 418 def add_filters(fields, operators, values)
419 419 if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
420 420 fields.each do |field|
421 421 add_filter(field, operators[field], values && values[field])
422 422 end
423 423 end
424 424 end
425 425
426 426 def has_filter?(field)
427 427 filters and filters[field]
428 428 end
429 429
430 430 def type_for(field)
431 431 available_filters[field][:type] if available_filters.has_key?(field)
432 432 end
433 433
434 434 def operator_for(field)
435 435 has_filter?(field) ? filters[field][:operator] : nil
436 436 end
437 437
438 438 def values_for(field)
439 439 has_filter?(field) ? filters[field][:values] : nil
440 440 end
441 441
442 442 def value_for(field, index=0)
443 443 (values_for(field) || [])[index]
444 444 end
445 445
446 446 def label_for(field)
447 447 label = available_filters[field][:name] if available_filters.has_key?(field)
448 448 label ||= l("field_#{field.to_s.gsub(/_id$/, '')}", :default => field)
449 449 end
450 450
451 451 def available_columns
452 452 return @available_columns if @available_columns
453 453 @available_columns = ::Query.available_columns.dup
454 454 @available_columns += (project ?
455 455 project.all_issue_custom_fields :
456 456 IssueCustomField.find(:all)
457 457 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
458 458
459 459 if User.current.allowed_to?(:view_time_entries, project, :global => true)
460 460 index = nil
461 461 @available_columns.each_with_index {|column, i| index = i if column.name == :estimated_hours}
462 462 index = (index ? index + 1 : -1)
463 463 # insert the column after estimated_hours or at the end
464 464 @available_columns.insert index, QueryColumn.new(:spent_hours,
465 465 :sortable => "(SELECT COALESCE(SUM(hours), 0) FROM #{TimeEntry.table_name} WHERE #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id)",
466 466 :default_order => 'desc',
467 467 :caption => :label_spent_time
468 468 )
469 469 end
470 470
471 471 if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
472 472 User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
473 473 @available_columns << QueryColumn.new(:is_private, :sortable => "#{Issue.table_name}.is_private")
474 474 end
475 475
476 476 disabled_fields = Tracker.disabled_core_fields(trackers).map {|field| field.sub(/_id$/, '')}
477 477 @available_columns.reject! {|column|
478 478 disabled_fields.include?(column.name.to_s)
479 479 }
480 480
481 481 @available_columns
482 482 end
483 483
484 484 def self.available_columns=(v)
485 485 self.available_columns = (v)
486 486 end
487 487
488 488 def self.add_available_column(column)
489 489 self.available_columns << (column) if column.is_a?(QueryColumn)
490 490 end
491 491
492 492 # Returns an array of columns that can be used to group the results
493 493 def groupable_columns
494 494 available_columns.select {|c| c.groupable}
495 495 end
496 496
497 497 # Returns a Hash of columns and the key for sorting
498 498 def sortable_columns
499 499 {'id' => "#{Issue.table_name}.id"}.merge(available_columns.inject({}) {|h, column|
500 500 h[column.name.to_s] = column.sortable
501 501 h
502 502 })
503 503 end
504 504
505 505 def columns
506 506 # preserve the column_names order
507 507 (has_default_columns? ? default_columns_names : column_names).collect do |name|
508 508 available_columns.find { |col| col.name == name }
509 509 end.compact
510 510 end
511 511
512 512 def default_columns_names
513 513 @default_columns_names ||= begin
514 514 default_columns = Setting.issue_list_default_columns.map(&:to_sym)
515 515
516 516 project.present? ? default_columns : [:project] | default_columns
517 517 end
518 518 end
519 519
520 520 def column_names=(names)
521 521 if names
522 522 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
523 523 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
524 524 # Set column_names to nil if default columns
525 525 if names == default_columns_names
526 526 names = nil
527 527 end
528 528 end
529 529 write_attribute(:column_names, names)
530 530 end
531 531
532 532 def has_column?(column)
533 533 column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column)
534 534 end
535 535
536 536 def has_default_columns?
537 537 column_names.nil? || column_names.empty?
538 538 end
539 539
540 540 def sort_criteria=(arg)
541 541 c = []
542 542 if arg.is_a?(Hash)
543 543 arg = arg.keys.sort.collect {|k| arg[k]}
544 544 end
545 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']}
545 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, (o == 'desc' || o == false) ? 'desc' : 'asc']}
546 546 write_attribute(:sort_criteria, c)
547 547 end
548 548
549 549 def sort_criteria
550 550 read_attribute(:sort_criteria) || []
551 551 end
552 552
553 553 def sort_criteria_key(arg)
554 554 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
555 555 end
556 556
557 557 def sort_criteria_order(arg)
558 558 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
559 559 end
560 560
561 def sort_criteria_order_for(key)
562 sort_criteria.detect {|k, order| key.to_s == k}.try(:last)
563 end
564
561 565 # Returns the SQL sort order that should be prepended for grouping
562 566 def group_by_sort_order
563 567 if grouped? && (column = group_by_column)
568 order = sort_criteria_order_for(column.name) || column.default_order
564 569 column.sortable.is_a?(Array) ?
565 column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') :
566 "#{column.sortable} #{column.default_order}"
570 column.sortable.collect {|s| "#{s} #{order}"}.join(',') :
571 "#{column.sortable} #{order}"
567 572 end
568 573 end
569 574
570 575 # Returns true if the query is a grouped query
571 576 def grouped?
572 577 !group_by_column.nil?
573 578 end
574 579
575 580 def group_by_column
576 581 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
577 582 end
578 583
579 584 def group_by_statement
580 585 group_by_column.try(:groupable)
581 586 end
582 587
583 588 def project_statement
584 589 project_clauses = []
585 590 if project && !project.descendants.active.empty?
586 591 ids = [project.id]
587 592 if has_filter?("subproject_id")
588 593 case operator_for("subproject_id")
589 594 when '='
590 595 # include the selected subprojects
591 596 ids += values_for("subproject_id").each(&:to_i)
592 597 when '!*'
593 598 # main project only
594 599 else
595 600 # all subprojects
596 601 ids += project.descendants.collect(&:id)
597 602 end
598 603 elsif Setting.display_subprojects_issues?
599 604 ids += project.descendants.collect(&:id)
600 605 end
601 606 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
602 607 elsif project
603 608 project_clauses << "#{Project.table_name}.id = %d" % project.id
604 609 end
605 610 project_clauses.any? ? project_clauses.join(' AND ') : nil
606 611 end
607 612
608 613 def statement
609 614 # filters clauses
610 615 filters_clauses = []
611 616 filters.each_key do |field|
612 617 next if field == "subproject_id"
613 618 v = values_for(field).clone
614 619 next unless v and !v.empty?
615 620 operator = operator_for(field)
616 621
617 622 # "me" value subsitution
618 623 if %w(assigned_to_id author_id watcher_id).include?(field)
619 624 if v.delete("me")
620 625 if User.current.logged?
621 626 v.push(User.current.id.to_s)
622 627 v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id'
623 628 else
624 629 v.push("0")
625 630 end
626 631 end
627 632 end
628 633
629 634 if field == 'project_id'
630 635 if v.delete('mine')
631 636 v += User.current.memberships.map(&:project_id).map(&:to_s)
632 637 end
633 638 end
634 639
635 640 if field =~ /cf_(\d+)$/
636 641 # custom field
637 642 filters_clauses << sql_for_custom_field(field, operator, v, $1)
638 643 elsif respond_to?("sql_for_#{field}_field")
639 644 # specific statement
640 645 filters_clauses << send("sql_for_#{field}_field", field, operator, v)
641 646 else
642 647 # regular field
643 648 filters_clauses << '(' + sql_for_field(field, operator, v, Issue.table_name, field) + ')'
644 649 end
645 650 end if filters and valid?
646 651
647 652 filters_clauses << project_statement
648 653 filters_clauses.reject!(&:blank?)
649 654
650 655 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
651 656 end
652 657
653 658 # Returns the issue count
654 659 def issue_count
655 660 Issue.visible.count(:include => [:status, :project], :conditions => statement)
656 661 rescue ::ActiveRecord::StatementInvalid => e
657 662 raise StatementInvalid.new(e.message)
658 663 end
659 664
660 665 # Returns the issue count by group or nil if query is not grouped
661 666 def issue_count_by_group
662 667 r = nil
663 668 if grouped?
664 669 begin
665 670 # Rails3 will raise an (unexpected) RecordNotFound if there's only a nil group value
666 671 r = Issue.visible.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement)
667 672 rescue ActiveRecord::RecordNotFound
668 673 r = {nil => issue_count}
669 674 end
670 675 c = group_by_column
671 676 if c.is_a?(QueryCustomFieldColumn)
672 677 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
673 678 end
674 679 end
675 680 r
676 681 rescue ::ActiveRecord::StatementInvalid => e
677 682 raise StatementInvalid.new(e.message)
678 683 end
679 684
680 685 # Returns the issues
681 686 # Valid options are :order, :offset, :limit, :include, :conditions
682 687 def issues(options={})
683 688 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
684 689 order_option = nil if order_option.blank?
685 690
686 691 issues = Issue.visible.scoped(:conditions => options[:conditions]).find :all, :include => ([:status, :project] + (options[:include] || [])).uniq,
687 692 :conditions => statement,
688 693 :order => order_option,
689 694 :joins => joins_for_order_statement(order_option),
690 695 :limit => options[:limit],
691 696 :offset => options[:offset]
692 697
693 698 if has_column?(:spent_hours)
694 699 Issue.load_visible_spent_hours(issues)
695 700 end
696 701 if has_column?(:relations)
697 702 Issue.load_visible_relations(issues)
698 703 end
699 704 issues
700 705 rescue ::ActiveRecord::StatementInvalid => e
701 706 raise StatementInvalid.new(e.message)
702 707 end
703 708
704 709 # Returns the issues ids
705 710 def issue_ids(options={})
706 711 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
707 712 order_option = nil if order_option.blank?
708 713
709 714 Issue.visible.scoped(:conditions => options[:conditions]).scoped(:include => ([:status, :project] + (options[:include] || [])).uniq,
710 715 :conditions => statement,
711 716 :order => order_option,
712 717 :joins => joins_for_order_statement(order_option),
713 718 :limit => options[:limit],
714 719 :offset => options[:offset]).find_ids
715 720 rescue ::ActiveRecord::StatementInvalid => e
716 721 raise StatementInvalid.new(e.message)
717 722 end
718 723
719 724 # Returns the journals
720 725 # Valid options are :order, :offset, :limit
721 726 def journals(options={})
722 727 Journal.visible.find :all, :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
723 728 :conditions => statement,
724 729 :order => options[:order],
725 730 :limit => options[:limit],
726 731 :offset => options[:offset]
727 732 rescue ::ActiveRecord::StatementInvalid => e
728 733 raise StatementInvalid.new(e.message)
729 734 end
730 735
731 736 # Returns the versions
732 737 # Valid options are :conditions
733 738 def versions(options={})
734 739 Version.visible.scoped(:conditions => options[:conditions]).find :all, :include => :project, :conditions => project_statement
735 740 rescue ::ActiveRecord::StatementInvalid => e
736 741 raise StatementInvalid.new(e.message)
737 742 end
738 743
739 744 def sql_for_watcher_id_field(field, operator, value)
740 745 db_table = Watcher.table_name
741 746 "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND " +
742 747 sql_for_field(field, '=', value, db_table, 'user_id') + ')'
743 748 end
744 749
745 750 def sql_for_member_of_group_field(field, operator, value)
746 751 if operator == '*' # Any group
747 752 groups = Group.all
748 753 operator = '=' # Override the operator since we want to find by assigned_to
749 754 elsif operator == "!*"
750 755 groups = Group.all
751 756 operator = '!' # Override the operator since we want to find by assigned_to
752 757 else
753 758 groups = Group.find_all_by_id(value)
754 759 end
755 760 groups ||= []
756 761
757 762 members_of_groups = groups.inject([]) {|user_ids, group|
758 763 if group && group.user_ids.present?
759 764 user_ids << group.user_ids
760 765 end
761 766 user_ids.flatten.uniq.compact
762 767 }.sort.collect(&:to_s)
763 768
764 769 '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
765 770 end
766 771
767 772 def sql_for_assigned_to_role_field(field, operator, value)
768 773 case operator
769 774 when "*", "!*" # Member / Not member
770 775 sw = operator == "!*" ? 'NOT' : ''
771 776 nl = operator == "!*" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
772 777 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}" +
773 778 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id))"
774 779 when "=", "!"
775 780 role_cond = value.any? ?
776 781 "#{MemberRole.table_name}.role_id IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" :
777 782 "1=0"
778 783
779 784 sw = operator == "!" ? 'NOT' : ''
780 785 nl = operator == "!" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
781 786 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}, #{MemberRole.table_name}" +
782 787 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id AND #{Member.table_name}.id = #{MemberRole.table_name}.member_id AND #{role_cond}))"
783 788 end
784 789 end
785 790
786 791 def sql_for_is_private_field(field, operator, value)
787 792 op = (operator == "=" ? 'IN' : 'NOT IN')
788 793 va = value.map {|v| v == '0' ? connection.quoted_false : connection.quoted_true}.uniq.join(',')
789 794
790 795 "#{Issue.table_name}.is_private #{op} (#{va})"
791 796 end
792 797
793 798 def sql_for_relations(field, operator, value, options={})
794 799 relation_options = IssueRelation::TYPES[field]
795 800 return relation_options unless relation_options
796 801
797 802 relation_type = field
798 803 join_column, target_join_column = "issue_from_id", "issue_to_id"
799 804 if relation_options[:reverse] || options[:reverse]
800 805 relation_type = relation_options[:reverse] || relation_type
801 806 join_column, target_join_column = target_join_column, join_column
802 807 end
803 808
804 809 sql = case operator
805 810 when "*", "!*"
806 811 op = (operator == "*" ? 'IN' : 'NOT IN')
807 812 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name} WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}')"
808 813 when "=", "!"
809 814 op = (operator == "=" ? 'IN' : 'NOT IN')
810 815 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name} WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = #{value.first.to_i})"
811 816 when "=p", "=!p", "!p"
812 817 op = (operator == "!p" ? 'NOT IN' : 'IN')
813 818 comp = (operator == "=!p" ? '<>' : '=')
814 819 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name}, #{Issue.table_name} relissues WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = relissues.id AND relissues.project_id #{comp} #{value.first.to_i})"
815 820 end
816 821
817 822 if relation_options[:sym] == field && !options[:reverse]
818 823 sqls = [sql, sql_for_relations(field, operator, value, :reverse => true)]
819 824 sqls.join(["!", "!*", "!p"].include?(operator) ? " AND " : " OR ")
820 825 else
821 826 sql
822 827 end
823 828 end
824 829
825 830 IssueRelation::TYPES.keys.each do |relation_type|
826 831 alias_method "sql_for_#{relation_type}_field".to_sym, :sql_for_relations
827 832 end
828 833
829 834 private
830 835
831 836 def sql_for_custom_field(field, operator, value, custom_field_id)
832 837 db_table = CustomValue.table_name
833 838 db_field = 'value'
834 839 filter = @available_filters[field]
835 840 return nil unless filter
836 841 if filter[:format] == 'user'
837 842 if value.delete('me')
838 843 value.push User.current.id.to_s
839 844 end
840 845 end
841 846 not_in = nil
842 847 if operator == '!'
843 848 # Makes ! operator work for custom fields with multiple values
844 849 operator = '='
845 850 not_in = 'NOT'
846 851 end
847 852 customized_key = "id"
848 853 customized_class = Issue
849 854 if field =~ /^(.+)\.cf_/
850 855 assoc = $1
851 856 customized_key = "#{assoc}_id"
852 857 customized_class = Issue.reflect_on_association(assoc.to_sym).klass.base_class rescue nil
853 858 raise "Unknown Issue association #{assoc}" unless customized_class
854 859 end
855 860 "#{Issue.table_name}.#{customized_key} #{not_in} IN (SELECT #{customized_class.table_name}.id FROM #{customized_class.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='#{customized_class}' AND #{db_table}.customized_id=#{customized_class.table_name}.id AND #{db_table}.custom_field_id=#{custom_field_id} WHERE " +
856 861 sql_for_field(field, operator, value, db_table, db_field, true) + ')'
857 862 end
858 863
859 864 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
860 865 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
861 866 sql = ''
862 867 case operator
863 868 when "="
864 869 if value.any?
865 870 case type_for(field)
866 871 when :date, :date_past
867 872 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), (Date.parse(value.first) rescue nil))
868 873 when :integer
869 874 if is_custom_filter
870 875 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) = #{value.first.to_i})"
871 876 else
872 877 sql = "#{db_table}.#{db_field} = #{value.first.to_i}"
873 878 end
874 879 when :float
875 880 if is_custom_filter
876 881 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5})"
877 882 else
878 883 sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}"
879 884 end
880 885 else
881 886 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
882 887 end
883 888 else
884 889 # IN an empty set
885 890 sql = "1=0"
886 891 end
887 892 when "!"
888 893 if value.any?
889 894 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
890 895 else
891 896 # NOT IN an empty set
892 897 sql = "1=1"
893 898 end
894 899 when "!*"
895 900 sql = "#{db_table}.#{db_field} IS NULL"
896 901 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
897 902 when "*"
898 903 sql = "#{db_table}.#{db_field} IS NOT NULL"
899 904 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
900 905 when ">="
901 906 if [:date, :date_past].include?(type_for(field))
902 907 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), nil)
903 908 else
904 909 if is_custom_filter
905 910 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) >= #{value.first.to_f})"
906 911 else
907 912 sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
908 913 end
909 914 end
910 915 when "<="
911 916 if [:date, :date_past].include?(type_for(field))
912 917 sql = date_clause(db_table, db_field, nil, (Date.parse(value.first) rescue nil))
913 918 else
914 919 if is_custom_filter
915 920 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) <= #{value.first.to_f})"
916 921 else
917 922 sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
918 923 end
919 924 end
920 925 when "><"
921 926 if [:date, :date_past].include?(type_for(field))
922 927 sql = date_clause(db_table, db_field, (Date.parse(value[0]) rescue nil), (Date.parse(value[1]) rescue nil))
923 928 else
924 929 if is_custom_filter
925 930 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value[0].to_f} AND #{value[1].to_f})"
926 931 else
927 932 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
928 933 end
929 934 end
930 935 when "o"
931 936 sql = "#{Issue.table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_false})" if field == "status_id"
932 937 when "c"
933 938 sql = "#{Issue.table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_true})" if field == "status_id"
934 939 when ">t-"
935 940 sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0)
936 941 when "<t-"
937 942 sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i)
938 943 when "t-"
939 944 sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
940 945 when ">t+"
941 946 sql = relative_date_clause(db_table, db_field, value.first.to_i, nil)
942 947 when "<t+"
943 948 sql = relative_date_clause(db_table, db_field, 0, value.first.to_i)
944 949 when "t+"
945 950 sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i)
946 951 when "t"
947 952 sql = relative_date_clause(db_table, db_field, 0, 0)
948 953 when "w"
949 954 first_day_of_week = l(:general_first_day_of_week).to_i
950 955 day_of_week = Date.today.cwday
951 956 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
952 957 sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6)
953 958 when "~"
954 959 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
955 960 when "!~"
956 961 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
957 962 else
958 963 raise "Unknown query operator #{operator}"
959 964 end
960 965
961 966 return sql
962 967 end
963 968
964 969 def add_custom_fields_filters(custom_fields, assoc=nil)
965 970 return unless custom_fields.present?
966 971 @available_filters ||= {}
967 972
968 973 custom_fields.select(&:is_filter?).each do |field|
969 974 case field.field_format
970 975 when "text"
971 976 options = { :type => :text, :order => 20 }
972 977 when "list"
973 978 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
974 979 when "date"
975 980 options = { :type => :date, :order => 20 }
976 981 when "bool"
977 982 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
978 983 when "int"
979 984 options = { :type => :integer, :order => 20 }
980 985 when "float"
981 986 options = { :type => :float, :order => 20 }
982 987 when "user", "version"
983 988 next unless project
984 989 values = field.possible_values_options(project)
985 990 if User.current.logged? && field.field_format == 'user'
986 991 values.unshift ["<< #{l(:label_me)} >>", "me"]
987 992 end
988 993 options = { :type => :list_optional, :values => values, :order => 20}
989 994 else
990 995 options = { :type => :string, :order => 20 }
991 996 end
992 997 filter_id = "cf_#{field.id}"
993 998 filter_name = field.name
994 999 if assoc.present?
995 1000 filter_id = "#{assoc}.#{filter_id}"
996 1001 filter_name = l("label_attribute_of_#{assoc}", :name => filter_name)
997 1002 end
998 1003 @available_filters[filter_id] = options.merge({
999 1004 :name => filter_name,
1000 1005 :format => field.field_format,
1001 1006 :field => field
1002 1007 })
1003 1008 end
1004 1009 end
1005 1010
1006 1011 def add_associations_custom_fields_filters(*associations)
1007 1012 fields_by_class = CustomField.where(:is_filter => true).group_by(&:class)
1008 1013 associations.each do |assoc|
1009 1014 association_klass = Issue.reflect_on_association(assoc).klass
1010 1015 fields_by_class.each do |field_class, fields|
1011 1016 if field_class.customized_class <= association_klass
1012 1017 add_custom_fields_filters(fields, assoc)
1013 1018 end
1014 1019 end
1015 1020 end
1016 1021 end
1017 1022
1018 1023 # Returns a SQL clause for a date or datetime field.
1019 1024 def date_clause(table, field, from, to)
1020 1025 s = []
1021 1026 if from
1022 1027 from_yesterday = from - 1
1023 1028 from_yesterday_time = Time.local(from_yesterday.year, from_yesterday.month, from_yesterday.day)
1024 1029 if self.class.default_timezone == :utc
1025 1030 from_yesterday_time = from_yesterday_time.utc
1026 1031 end
1027 1032 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date(from_yesterday_time.end_of_day)])
1028 1033 end
1029 1034 if to
1030 1035 to_time = Time.local(to.year, to.month, to.day)
1031 1036 if self.class.default_timezone == :utc
1032 1037 to_time = to_time.utc
1033 1038 end
1034 1039 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date(to_time.end_of_day)])
1035 1040 end
1036 1041 s.join(' AND ')
1037 1042 end
1038 1043
1039 1044 # Returns a SQL clause for a date or datetime field using relative dates.
1040 1045 def relative_date_clause(table, field, days_from, days_to)
1041 1046 date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil))
1042 1047 end
1043 1048
1044 1049 # Additional joins required for the given sort options
1045 1050 def joins_for_order_statement(order_options)
1046 1051 joins = []
1047 1052
1048 1053 if order_options
1049 1054 if order_options.include?('authors')
1050 1055 joins << "LEFT OUTER JOIN #{User.table_name} authors ON authors.id = #{Issue.table_name}.author_id"
1051 1056 end
1052 1057 order_options.scan(/cf_\d+/).uniq.each do |name|
1053 1058 column = available_columns.detect {|c| c.name.to_s == name}
1054 1059 join = column && column.custom_field.join_for_order_statement
1055 1060 if join
1056 1061 joins << join
1057 1062 end
1058 1063 end
1059 1064 end
1060 1065
1061 1066 joins.any? ? joins.join(' ') : nil
1062 1067 end
1063 1068 end
@@ -1,3806 +1,3826
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
65 65 # links to visible issues
66 66 assert_select 'a[href=/issues/1]', :text => /Can&#x27;t print recipes/
67 67 assert_select 'a[href=/issues/5]', :text => /Subproject issue/
68 68 # private projects hidden
69 69 assert_select 'a[href=/issues/6]', 0
70 70 assert_select 'a[href=/issues/4]', 0
71 71 # project column
72 72 assert_select 'th', :text => /Project/
73 73 end
74 74 end
75 75
76 76 def test_index_should_not_list_issues_when_module_disabled
77 77 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
78 78 get :index
79 79 assert_response :success
80 80 assert_template 'index'
81 81 assert_not_nil assigns(:issues)
82 82 assert_nil assigns(:project)
83 83
84 84 assert_select 'a[href=/issues/1]', 0
85 85 assert_select 'a[href=/issues/5]', :text => /Subproject issue/
86 86 end
87 87
88 88 def test_index_should_list_visible_issues_only
89 89 get :index, :per_page => 100
90 90 assert_response :success
91 91 assert_not_nil assigns(:issues)
92 92 assert_nil assigns(:issues).detect {|issue| !issue.visible?}
93 93 end
94 94
95 95 def test_index_with_project
96 96 Setting.display_subprojects_issues = 0
97 97 get :index, :project_id => 1
98 98 assert_response :success
99 99 assert_template 'index'
100 100 assert_not_nil assigns(:issues)
101 101
102 102 assert_select 'a[href=/issues/1]', :text => /Can&#x27;t print recipes/
103 103 assert_select 'a[href=/issues/5]', 0
104 104 end
105 105
106 106 def test_index_with_project_and_subprojects
107 107 Setting.display_subprojects_issues = 1
108 108 get :index, :project_id => 1
109 109 assert_response :success
110 110 assert_template 'index'
111 111 assert_not_nil assigns(:issues)
112 112
113 113 assert_select 'a[href=/issues/1]', :text => /Can&#x27;t print recipes/
114 114 assert_select 'a[href=/issues/5]', :text => /Subproject issue/
115 115 assert_select 'a[href=/issues/6]', 0
116 116 end
117 117
118 118 def test_index_with_project_and_subprojects_should_show_private_subprojects_with_permission
119 119 @request.session[:user_id] = 2
120 120 Setting.display_subprojects_issues = 1
121 121 get :index, :project_id => 1
122 122 assert_response :success
123 123 assert_template 'index'
124 124 assert_not_nil assigns(:issues)
125 125
126 126 assert_select 'a[href=/issues/1]', :text => /Can&#x27;t print recipes/
127 127 assert_select 'a[href=/issues/5]', :text => /Subproject issue/
128 128 assert_select 'a[href=/issues/6]', :text => /Issue of a private subproject/
129 129 end
130 130
131 131 def test_index_with_project_and_default_filter
132 132 get :index, :project_id => 1, :set_filter => 1
133 133 assert_response :success
134 134 assert_template 'index'
135 135 assert_not_nil assigns(:issues)
136 136
137 137 query = assigns(:query)
138 138 assert_not_nil query
139 139 # default filter
140 140 assert_equal({'status_id' => {:operator => 'o', :values => ['']}}, query.filters)
141 141 end
142 142
143 143 def test_index_with_project_and_filter
144 144 get :index, :project_id => 1, :set_filter => 1,
145 145 :f => ['tracker_id'],
146 146 :op => {'tracker_id' => '='},
147 147 :v => {'tracker_id' => ['1']}
148 148 assert_response :success
149 149 assert_template 'index'
150 150 assert_not_nil assigns(:issues)
151 151
152 152 query = assigns(:query)
153 153 assert_not_nil query
154 154 assert_equal({'tracker_id' => {:operator => '=', :values => ['1']}}, query.filters)
155 155 end
156 156
157 157 def test_index_with_short_filters
158 158 to_test = {
159 159 'status_id' => {
160 160 'o' => { :op => 'o', :values => [''] },
161 161 'c' => { :op => 'c', :values => [''] },
162 162 '7' => { :op => '=', :values => ['7'] },
163 163 '7|3|4' => { :op => '=', :values => ['7', '3', '4'] },
164 164 '=7' => { :op => '=', :values => ['7'] },
165 165 '!3' => { :op => '!', :values => ['3'] },
166 166 '!7|3|4' => { :op => '!', :values => ['7', '3', '4'] }},
167 167 'subject' => {
168 168 'This is a subject' => { :op => '=', :values => ['This is a subject'] },
169 169 'o' => { :op => '=', :values => ['o'] },
170 170 '~This is part of a subject' => { :op => '~', :values => ['This is part of a subject'] },
171 171 '!~This is part of a subject' => { :op => '!~', :values => ['This is part of a subject'] }},
172 172 'tracker_id' => {
173 173 '3' => { :op => '=', :values => ['3'] },
174 174 '=3' => { :op => '=', :values => ['3'] }},
175 175 'start_date' => {
176 176 '2011-10-12' => { :op => '=', :values => ['2011-10-12'] },
177 177 '=2011-10-12' => { :op => '=', :values => ['2011-10-12'] },
178 178 '>=2011-10-12' => { :op => '>=', :values => ['2011-10-12'] },
179 179 '<=2011-10-12' => { :op => '<=', :values => ['2011-10-12'] },
180 180 '><2011-10-01|2011-10-30' => { :op => '><', :values => ['2011-10-01', '2011-10-30'] },
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 't' => { :op => 't', :values => [''] },
185 185 'w' => { :op => 'w', :values => [''] },
186 186 '>t-2' => { :op => '>t-', :values => ['2'] },
187 187 '<t-2' => { :op => '<t-', :values => ['2'] },
188 188 't-2' => { :op => 't-', :values => ['2'] }},
189 189 'created_on' => {
190 190 '>=2011-10-12' => { :op => '>=', :values => ['2011-10-12'] },
191 191 '<t-2' => { :op => '<t-', :values => ['2'] },
192 192 '>t-2' => { :op => '>t-', :values => ['2'] },
193 193 't-2' => { :op => 't-', :values => ['2'] }},
194 194 'cf_1' => {
195 195 'c' => { :op => '=', :values => ['c'] },
196 196 '!c' => { :op => '!', :values => ['c'] },
197 197 '!*' => { :op => '!*', :values => [''] },
198 198 '*' => { :op => '*', :values => [''] }},
199 199 'estimated_hours' => {
200 200 '=13.4' => { :op => '=', :values => ['13.4'] },
201 201 '>=45' => { :op => '>=', :values => ['45'] },
202 202 '<=125' => { :op => '<=', :values => ['125'] },
203 203 '><10.5|20.5' => { :op => '><', :values => ['10.5', '20.5'] },
204 204 '!*' => { :op => '!*', :values => [''] },
205 205 '*' => { :op => '*', :values => [''] }}
206 206 }
207 207
208 208 default_filter = { 'status_id' => {:operator => 'o', :values => [''] }}
209 209
210 210 to_test.each do |field, expression_and_expected|
211 211 expression_and_expected.each do |filter_expression, expected|
212 212
213 213 get :index, :set_filter => 1, field => filter_expression
214 214
215 215 assert_response :success
216 216 assert_template 'index'
217 217 assert_not_nil assigns(:issues)
218 218
219 219 query = assigns(:query)
220 220 assert_not_nil query
221 221 assert query.has_filter?(field)
222 222 assert_equal(default_filter.merge({field => {:operator => expected[:op], :values => expected[:values]}}), query.filters)
223 223 end
224 224 end
225 225 end
226 226
227 227 def test_index_with_project_and_empty_filters
228 228 get :index, :project_id => 1, :set_filter => 1, :fields => ['']
229 229 assert_response :success
230 230 assert_template 'index'
231 231 assert_not_nil assigns(:issues)
232 232
233 233 query = assigns(:query)
234 234 assert_not_nil query
235 235 # no filter
236 236 assert_equal({}, query.filters)
237 237 end
238 238
239 239 def test_index_with_project_custom_field_filter
240 240 field = ProjectCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
241 241 CustomValue.create!(:custom_field => field, :customized => Project.find(3), :value => 'Foo')
242 242 CustomValue.create!(:custom_field => field, :customized => Project.find(5), :value => 'Foo')
243 243 filter_name = "project.cf_#{field.id}"
244 244 @request.session[:user_id] = 1
245 245
246 246 get :index, :set_filter => 1,
247 247 :f => [filter_name],
248 248 :op => {filter_name => '='},
249 249 :v => {filter_name => ['Foo']}
250 250 assert_response :success
251 251 assert_template 'index'
252 252 assert_equal [3, 5], assigns(:issues).map(&:project_id).uniq.sort
253 253 end
254 254
255 255 def test_index_with_query
256 256 get :index, :project_id => 1, :query_id => 5
257 257 assert_response :success
258 258 assert_template 'index'
259 259 assert_not_nil assigns(:issues)
260 260 assert_nil assigns(:issue_count_by_group)
261 261 end
262 262
263 263 def test_index_with_query_grouped_by_tracker
264 264 get :index, :project_id => 1, :query_id => 6
265 265 assert_response :success
266 266 assert_template 'index'
267 267 assert_not_nil assigns(:issues)
268 268 assert_not_nil assigns(:issue_count_by_group)
269 269 end
270 270
271 271 def test_index_with_query_grouped_by_list_custom_field
272 272 get :index, :project_id => 1, :query_id => 9
273 273 assert_response :success
274 274 assert_template 'index'
275 275 assert_not_nil assigns(:issues)
276 276 assert_not_nil assigns(:issue_count_by_group)
277 277 end
278 278
279 279 def test_index_with_query_grouped_by_user_custom_field
280 280 cf = IssueCustomField.create!(:name => 'User', :is_for_all => true, :tracker_ids => [1,2,3], :field_format => 'user')
281 281 CustomValue.create!(:custom_field => cf, :customized => Issue.find(1), :value => '2')
282 282 CustomValue.create!(:custom_field => cf, :customized => Issue.find(2), :value => '3')
283 283 CustomValue.create!(:custom_field => cf, :customized => Issue.find(3), :value => '3')
284 284 CustomValue.create!(:custom_field => cf, :customized => Issue.find(5), :value => '')
285 285
286 286 get :index, :project_id => 1, :set_filter => 1, :group_by => "cf_#{cf.id}"
287 287 assert_response :success
288 288
289 289 assert_select 'tr.group', 3
290 290 assert_select 'tr.group' do
291 291 assert_select 'a', :text => 'John Smith'
292 292 assert_select 'span.count', :text => '1'
293 293 end
294 294 assert_select 'tr.group' do
295 295 assert_select 'a', :text => 'Dave Lopper'
296 296 assert_select 'span.count', :text => '2'
297 297 end
298 298 end
299 299
300 def test_index_with_query_grouped_by_tracker
301 3.times {|i| Issue.generate!(:tracker_id => (i + 1))}
302
303 get :index, :set_filter => 1, :group_by => 'tracker', :sort => 'id:desc'
304 assert_response :success
305
306 trackers = assigns(:issues).map(&:tracker).uniq
307 assert_equal [1, 2, 3], trackers.map(&:id)
308 end
309
310 def test_index_with_query_grouped_by_tracker_in_reverse_order
311 3.times {|i| Issue.generate!(:tracker_id => (i + 1))}
312
313 get :index, :set_filter => 1, :group_by => 'tracker', :sort => 'id:desc,tracker:desc'
314 assert_response :success
315
316 trackers = assigns(:issues).map(&:tracker).uniq
317 assert_equal [3, 2, 1], trackers.map(&:id)
318 end
319
300 320 def test_index_with_query_id_and_project_id_should_set_session_query
301 321 get :index, :project_id => 1, :query_id => 4
302 322 assert_response :success
303 323 assert_kind_of Hash, session[:query]
304 324 assert_equal 4, session[:query][:id]
305 325 assert_equal 1, session[:query][:project_id]
306 326 end
307 327
308 328 def test_index_with_invalid_query_id_should_respond_404
309 329 get :index, :project_id => 1, :query_id => 999
310 330 assert_response 404
311 331 end
312 332
313 333 def test_index_with_cross_project_query_in_session_should_show_project_issues
314 334 q = Query.create!(:name => "test", :user_id => 2, :is_public => false, :project => nil)
315 335 @request.session[:query] = {:id => q.id, :project_id => 1}
316 336
317 337 with_settings :display_subprojects_issues => '0' do
318 338 get :index, :project_id => 1
319 339 end
320 340 assert_response :success
321 341 assert_not_nil assigns(:query)
322 342 assert_equal q.id, assigns(:query).id
323 343 assert_equal 1, assigns(:query).project_id
324 344 assert_equal [1], assigns(:issues).map(&:project_id).uniq
325 345 end
326 346
327 347 def test_private_query_should_not_be_available_to_other_users
328 348 q = Query.create!(:name => "private", :user => User.find(2), :is_public => false, :project => nil)
329 349 @request.session[:user_id] = 3
330 350
331 351 get :index, :query_id => q.id
332 352 assert_response 403
333 353 end
334 354
335 355 def test_private_query_should_be_available_to_its_user
336 356 q = Query.create!(:name => "private", :user => User.find(2), :is_public => false, :project => nil)
337 357 @request.session[:user_id] = 2
338 358
339 359 get :index, :query_id => q.id
340 360 assert_response :success
341 361 end
342 362
343 363 def test_public_query_should_be_available_to_other_users
344 364 q = Query.create!(:name => "private", :user => User.find(2), :is_public => true, :project => nil)
345 365 @request.session[:user_id] = 3
346 366
347 367 get :index, :query_id => q.id
348 368 assert_response :success
349 369 end
350 370
351 371 def test_index_should_omit_page_param_in_export_links
352 372 get :index, :page => 2
353 373 assert_response :success
354 374 assert_select 'a.atom[href=/issues.atom]'
355 375 assert_select 'a.csv[href=/issues.csv]'
356 376 assert_select 'a.pdf[href=/issues.pdf]'
357 377 assert_select 'form#csv-export-form[action=/issues.csv]'
358 378 end
359 379
360 380 def test_index_csv
361 381 get :index, :format => 'csv'
362 382 assert_response :success
363 383 assert_not_nil assigns(:issues)
364 384 assert_equal 'text/csv; header=present', @response.content_type
365 385 assert @response.body.starts_with?("#,")
366 386 lines = @response.body.chomp.split("\n")
367 387 assert_equal assigns(:query).columns.size + 1, lines[0].split(',').size
368 388 end
369 389
370 390 def test_index_csv_with_project
371 391 get :index, :project_id => 1, :format => 'csv'
372 392 assert_response :success
373 393 assert_not_nil assigns(:issues)
374 394 assert_equal 'text/csv; header=present', @response.content_type
375 395 end
376 396
377 397 def test_index_csv_with_description
378 398 get :index, :format => 'csv', :description => '1'
379 399 assert_response :success
380 400 assert_not_nil assigns(:issues)
381 401 assert_equal 'text/csv; header=present', @response.content_type
382 402 assert @response.body.starts_with?("#,")
383 403 lines = @response.body.chomp.split("\n")
384 404 assert_equal assigns(:query).columns.size + 2, lines[0].split(',').size
385 405 end
386 406
387 407 def test_index_csv_with_spent_time_column
388 408 issue = Issue.create!(:project_id => 1, :tracker_id => 1, :subject => 'test_index_csv_with_spent_time_column', :author_id => 2)
389 409 TimeEntry.create!(:project => issue.project, :issue => issue, :hours => 7.33, :user => User.find(2), :spent_on => Date.today)
390 410
391 411 get :index, :format => 'csv', :set_filter => '1', :c => %w(subject spent_hours)
392 412 assert_response :success
393 413 assert_equal 'text/csv; header=present', @response.content_type
394 414 lines = @response.body.chomp.split("\n")
395 415 assert_include "#{issue.id},#{issue.subject},7.33", lines
396 416 end
397 417
398 418 def test_index_csv_with_all_columns
399 419 get :index, :format => 'csv', :columns => 'all'
400 420 assert_response :success
401 421 assert_not_nil assigns(:issues)
402 422 assert_equal 'text/csv; header=present', @response.content_type
403 423 assert @response.body.starts_with?("#,")
404 424 lines = @response.body.chomp.split("\n")
405 425 assert_equal assigns(:query).available_columns.size + 1, lines[0].split(',').size
406 426 end
407 427
408 428 def test_index_csv_with_multi_column_field
409 429 CustomField.find(1).update_attribute :multiple, true
410 430 issue = Issue.find(1)
411 431 issue.custom_field_values = {1 => ['MySQL', 'Oracle']}
412 432 issue.save!
413 433
414 434 get :index, :format => 'csv', :columns => 'all'
415 435 assert_response :success
416 436 lines = @response.body.chomp.split("\n")
417 437 assert lines.detect {|line| line.include?('"MySQL, Oracle"')}
418 438 end
419 439
420 440 def test_index_csv_big_5
421 441 with_settings :default_language => "zh-TW" do
422 442 str_utf8 = "\xe4\xb8\x80\xe6\x9c\x88"
423 443 str_big5 = "\xa4@\xa4\xeb"
424 444 if str_utf8.respond_to?(:force_encoding)
425 445 str_utf8.force_encoding('UTF-8')
426 446 str_big5.force_encoding('Big5')
427 447 end
428 448 issue = Issue.generate!(:subject => str_utf8)
429 449
430 450 get :index, :project_id => 1,
431 451 :f => ['subject'],
432 452 :op => '=', :values => [str_utf8],
433 453 :format => 'csv'
434 454 assert_equal 'text/csv; header=present', @response.content_type
435 455 lines = @response.body.chomp.split("\n")
436 456 s1 = "\xaa\xac\xbaA"
437 457 if str_utf8.respond_to?(:force_encoding)
438 458 s1.force_encoding('Big5')
439 459 end
440 460 assert lines[0].include?(s1)
441 461 assert lines[1].include?(str_big5)
442 462 end
443 463 end
444 464
445 465 def test_index_csv_cannot_convert_should_be_replaced_big_5
446 466 with_settings :default_language => "zh-TW" do
447 467 str_utf8 = "\xe4\xbb\xa5\xe5\x86\x85"
448 468 if str_utf8.respond_to?(:force_encoding)
449 469 str_utf8.force_encoding('UTF-8')
450 470 end
451 471 issue = Issue.generate!(:subject => str_utf8)
452 472
453 473 get :index, :project_id => 1,
454 474 :f => ['subject'],
455 475 :op => '=', :values => [str_utf8],
456 476 :c => ['status', 'subject'],
457 477 :format => 'csv',
458 478 :set_filter => 1
459 479 assert_equal 'text/csv; header=present', @response.content_type
460 480 lines = @response.body.chomp.split("\n")
461 481 s1 = "\xaa\xac\xbaA" # status
462 482 if str_utf8.respond_to?(:force_encoding)
463 483 s1.force_encoding('Big5')
464 484 end
465 485 assert lines[0].include?(s1)
466 486 s2 = lines[1].split(",")[2]
467 487 if s1.respond_to?(:force_encoding)
468 488 s3 = "\xa5H?" # subject
469 489 s3.force_encoding('Big5')
470 490 assert_equal s3, s2
471 491 elsif RUBY_PLATFORM == 'java'
472 492 assert_equal "??", s2
473 493 else
474 494 assert_equal "\xa5H???", s2
475 495 end
476 496 end
477 497 end
478 498
479 499 def test_index_csv_tw
480 500 with_settings :default_language => "zh-TW" do
481 501 str1 = "test_index_csv_tw"
482 502 issue = Issue.generate!(:subject => str1, :estimated_hours => '1234.5')
483 503
484 504 get :index, :project_id => 1,
485 505 :f => ['subject'],
486 506 :op => '=', :values => [str1],
487 507 :c => ['estimated_hours', 'subject'],
488 508 :format => 'csv',
489 509 :set_filter => 1
490 510 assert_equal 'text/csv; header=present', @response.content_type
491 511 lines = @response.body.chomp.split("\n")
492 512 assert_equal "#{issue.id},1234.50,#{str1}", lines[1]
493 513 end
494 514 end
495 515
496 516 def test_index_csv_fr
497 517 with_settings :default_language => "fr" do
498 518 str1 = "test_index_csv_fr"
499 519 issue = Issue.generate!(:subject => str1, :estimated_hours => '1234.5')
500 520
501 521 get :index, :project_id => 1,
502 522 :f => ['subject'],
503 523 :op => '=', :values => [str1],
504 524 :c => ['estimated_hours', 'subject'],
505 525 :format => 'csv',
506 526 :set_filter => 1
507 527 assert_equal 'text/csv; header=present', @response.content_type
508 528 lines = @response.body.chomp.split("\n")
509 529 assert_equal "#{issue.id};1234,50;#{str1}", lines[1]
510 530 end
511 531 end
512 532
513 533 def test_index_pdf
514 534 ["en", "zh", "zh-TW", "ja", "ko"].each do |lang|
515 535 with_settings :default_language => lang do
516 536
517 537 get :index
518 538 assert_response :success
519 539 assert_template 'index'
520 540
521 541 if lang == "ja"
522 542 if RUBY_PLATFORM != 'java'
523 543 assert_equal "CP932", l(:general_pdf_encoding)
524 544 end
525 545 if RUBY_PLATFORM == 'java' && l(:general_pdf_encoding) == "CP932"
526 546 next
527 547 end
528 548 end
529 549
530 550 get :index, :format => 'pdf'
531 551 assert_response :success
532 552 assert_not_nil assigns(:issues)
533 553 assert_equal 'application/pdf', @response.content_type
534 554
535 555 get :index, :project_id => 1, :format => 'pdf'
536 556 assert_response :success
537 557 assert_not_nil assigns(:issues)
538 558 assert_equal 'application/pdf', @response.content_type
539 559
540 560 get :index, :project_id => 1, :query_id => 6, :format => 'pdf'
541 561 assert_response :success
542 562 assert_not_nil assigns(:issues)
543 563 assert_equal 'application/pdf', @response.content_type
544 564 end
545 565 end
546 566 end
547 567
548 568 def test_index_pdf_with_query_grouped_by_list_custom_field
549 569 get :index, :project_id => 1, :query_id => 9, :format => 'pdf'
550 570 assert_response :success
551 571 assert_not_nil assigns(:issues)
552 572 assert_not_nil assigns(:issue_count_by_group)
553 573 assert_equal 'application/pdf', @response.content_type
554 574 end
555 575
556 576 def test_index_atom
557 577 get :index, :project_id => 'ecookbook', :format => 'atom'
558 578 assert_response :success
559 579 assert_template 'common/feed'
560 580 assert_equal 'application/atom+xml', response.content_type
561 581
562 582 assert_select 'feed' do
563 583 assert_select 'link[rel=self][href=?]', 'http://test.host/projects/ecookbook/issues.atom'
564 584 assert_select 'link[rel=alternate][href=?]', 'http://test.host/projects/ecookbook/issues'
565 585 assert_select 'entry link[href=?]', 'http://test.host/issues/1'
566 586 end
567 587 end
568 588
569 589 def test_index_sort
570 590 get :index, :sort => 'tracker,id:desc'
571 591 assert_response :success
572 592
573 593 sort_params = @request.session['issues_index_sort']
574 594 assert sort_params.is_a?(String)
575 595 assert_equal 'tracker,id:desc', sort_params
576 596
577 597 issues = assigns(:issues)
578 598 assert_not_nil issues
579 599 assert !issues.empty?
580 600 assert_equal issues.sort {|a,b| a.tracker == b.tracker ? b.id <=> a.id : a.tracker <=> b.tracker }.collect(&:id), issues.collect(&:id)
581 601 end
582 602
583 603 def test_index_sort_by_field_not_included_in_columns
584 604 Setting.issue_list_default_columns = %w(subject author)
585 605 get :index, :sort => 'tracker'
586 606 end
587 607
588 608 def test_index_sort_by_assigned_to
589 609 get :index, :sort => 'assigned_to'
590 610 assert_response :success
591 611 assignees = assigns(:issues).collect(&:assigned_to).compact
592 612 assert_equal assignees.sort, assignees
593 613 end
594 614
595 615 def test_index_sort_by_assigned_to_desc
596 616 get :index, :sort => 'assigned_to:desc'
597 617 assert_response :success
598 618 assignees = assigns(:issues).collect(&:assigned_to).compact
599 619 assert_equal assignees.sort.reverse, assignees
600 620 end
601 621
602 622 def test_index_group_by_assigned_to
603 623 get :index, :group_by => 'assigned_to', :sort => 'priority'
604 624 assert_response :success
605 625 end
606 626
607 627 def test_index_sort_by_author
608 628 get :index, :sort => 'author'
609 629 assert_response :success
610 630 authors = assigns(:issues).collect(&:author)
611 631 assert_equal authors.sort, authors
612 632 end
613 633
614 634 def test_index_sort_by_author_desc
615 635 get :index, :sort => 'author:desc'
616 636 assert_response :success
617 637 authors = assigns(:issues).collect(&:author)
618 638 assert_equal authors.sort.reverse, authors
619 639 end
620 640
621 641 def test_index_group_by_author
622 642 get :index, :group_by => 'author', :sort => 'priority'
623 643 assert_response :success
624 644 end
625 645
626 646 def test_index_sort_by_spent_hours
627 647 get :index, :sort => 'spent_hours:desc'
628 648 assert_response :success
629 649 hours = assigns(:issues).collect(&:spent_hours)
630 650 assert_equal hours.sort.reverse, hours
631 651 end
632 652
633 653 def test_index_sort_by_user_custom_field
634 654 cf = IssueCustomField.create!(:name => 'User', :is_for_all => true, :tracker_ids => [1,2,3], :field_format => 'user')
635 655 CustomValue.create!(:custom_field => cf, :customized => Issue.find(1), :value => '2')
636 656 CustomValue.create!(:custom_field => cf, :customized => Issue.find(2), :value => '3')
637 657 CustomValue.create!(:custom_field => cf, :customized => Issue.find(3), :value => '3')
638 658 CustomValue.create!(:custom_field => cf, :customized => Issue.find(5), :value => '')
639 659
640 660 get :index, :project_id => 1, :set_filter => 1, :sort => "cf_#{cf.id},id"
641 661 assert_response :success
642 662
643 663 assert_equal [2, 3, 1], assigns(:issues).select {|issue| issue.custom_field_value(cf).present?}.map(&:id)
644 664 end
645 665
646 666 def test_index_with_columns
647 667 columns = ['tracker', 'subject', 'assigned_to']
648 668 get :index, :set_filter => 1, :c => columns
649 669 assert_response :success
650 670
651 671 # query should use specified columns
652 672 query = assigns(:query)
653 673 assert_kind_of Query, query
654 674 assert_equal columns, query.column_names.map(&:to_s)
655 675
656 676 # columns should be stored in session
657 677 assert_kind_of Hash, session[:query]
658 678 assert_kind_of Array, session[:query][:column_names]
659 679 assert_equal columns, session[:query][:column_names].map(&:to_s)
660 680
661 681 # ensure only these columns are kept in the selected columns list
662 682 assert_select 'select#selected_columns option' do
663 683 assert_select 'option', 3
664 684 assert_select 'option[value=tracker]'
665 685 assert_select 'option[value=project]', 0
666 686 end
667 687 end
668 688
669 689 def test_index_without_project_should_implicitly_add_project_column_to_default_columns
670 690 Setting.issue_list_default_columns = ['tracker', 'subject', 'assigned_to']
671 691 get :index, :set_filter => 1
672 692
673 693 # query should use specified columns
674 694 query = assigns(:query)
675 695 assert_kind_of Query, query
676 696 assert_equal [:project, :tracker, :subject, :assigned_to], query.columns.map(&:name)
677 697 end
678 698
679 699 def test_index_without_project_and_explicit_default_columns_should_not_add_project_column
680 700 Setting.issue_list_default_columns = ['tracker', 'subject', 'assigned_to']
681 701 columns = ['tracker', 'subject', 'assigned_to']
682 702 get :index, :set_filter => 1, :c => columns
683 703
684 704 # query should use specified columns
685 705 query = assigns(:query)
686 706 assert_kind_of Query, query
687 707 assert_equal columns.map(&:to_sym), query.columns.map(&:name)
688 708 end
689 709
690 710 def test_index_with_custom_field_column
691 711 columns = %w(tracker subject cf_2)
692 712 get :index, :set_filter => 1, :c => columns
693 713 assert_response :success
694 714
695 715 # query should use specified columns
696 716 query = assigns(:query)
697 717 assert_kind_of Query, query
698 718 assert_equal columns, query.column_names.map(&:to_s)
699 719
700 720 assert_select 'table.issues td.cf_2.string'
701 721 end
702 722
703 723 def test_index_with_multi_custom_field_column
704 724 field = CustomField.find(1)
705 725 field.update_attribute :multiple, true
706 726 issue = Issue.find(1)
707 727 issue.custom_field_values = {1 => ['MySQL', 'Oracle']}
708 728 issue.save!
709 729
710 730 get :index, :set_filter => 1, :c => %w(tracker subject cf_1)
711 731 assert_response :success
712 732
713 733 assert_select 'table.issues td.cf_1', :text => 'MySQL, Oracle'
714 734 end
715 735
716 736 def test_index_with_multi_user_custom_field_column
717 737 field = IssueCustomField.create!(:name => 'Multi user', :field_format => 'user', :multiple => true,
718 738 :tracker_ids => [1], :is_for_all => true)
719 739 issue = Issue.find(1)
720 740 issue.custom_field_values = {field.id => ['2', '3']}
721 741 issue.save!
722 742
723 743 get :index, :set_filter => 1, :c => ['tracker', 'subject', "cf_#{field.id}"]
724 744 assert_response :success
725 745
726 746 assert_select "table.issues td.cf_#{field.id}" do
727 747 assert_select 'a', 2
728 748 assert_select 'a[href=?]', '/users/2', :text => 'John Smith'
729 749 assert_select 'a[href=?]', '/users/3', :text => 'Dave Lopper'
730 750 end
731 751 end
732 752
733 753 def test_index_with_date_column
734 754 with_settings :date_format => '%d/%m/%Y' do
735 755 Issue.find(1).update_attribute :start_date, '1987-08-24'
736 756
737 757 get :index, :set_filter => 1, :c => %w(start_date)
738 758
739 759 assert_select "table.issues td.start_date", :text => '24/08/1987'
740 760 end
741 761 end
742 762
743 763 def test_index_with_done_ratio_column
744 764 Issue.find(1).update_attribute :done_ratio, 40
745 765
746 766 get :index, :set_filter => 1, :c => %w(done_ratio)
747 767
748 768 assert_select 'table.issues td.done_ratio' do
749 769 assert_select 'table.progress' do
750 770 assert_select 'td.closed[style=?]', 'width: 40%;'
751 771 end
752 772 end
753 773 end
754 774
755 775 def test_index_with_spent_hours_column
756 776 get :index, :set_filter => 1, :c => %w(subject spent_hours)
757 777
758 778 assert_select 'table.issues tr#issue-3 td.spent_hours', :text => '1.00'
759 779 end
760 780
761 781 def test_index_should_not_show_spent_hours_column_without_permission
762 782 Role.anonymous.remove_permission! :view_time_entries
763 783 get :index, :set_filter => 1, :c => %w(subject spent_hours)
764 784
765 785 assert_select 'td.spent_hours', 0
766 786 end
767 787
768 788 def test_index_with_fixed_version_column
769 789 get :index, :set_filter => 1, :c => %w(fixed_version)
770 790
771 791 assert_select 'table.issues td.fixed_version' do
772 792 assert_select 'a[href=?]', '/versions/2', :text => '1.0'
773 793 end
774 794 end
775 795
776 796 def test_index_with_relations_column
777 797 IssueRelation.delete_all
778 798 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(7))
779 799 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(8), :issue_to => Issue.find(1))
780 800 IssueRelation.create!(:relation_type => "blocks", :issue_from => Issue.find(1), :issue_to => Issue.find(11))
781 801 IssueRelation.create!(:relation_type => "blocks", :issue_from => Issue.find(12), :issue_to => Issue.find(2))
782 802
783 803 get :index, :set_filter => 1, :c => %w(subject relations)
784 804 assert_response :success
785 805 assert_select "tr#issue-1 td.relations" do
786 806 assert_select "span", 3
787 807 assert_select "span", :text => "Related to #7"
788 808 assert_select "span", :text => "Related to #8"
789 809 assert_select "span", :text => "Blocks #11"
790 810 end
791 811 assert_select "tr#issue-2 td.relations" do
792 812 assert_select "span", 1
793 813 assert_select "span", :text => "Blocked by #12"
794 814 end
795 815 assert_select "tr#issue-3 td.relations" do
796 816 assert_select "span", 0
797 817 end
798 818
799 819 get :index, :set_filter => 1, :c => %w(relations), :format => 'csv'
800 820 assert_response :success
801 821 assert_equal 'text/csv; header=present', response.content_type
802 822 lines = response.body.chomp.split("\n")
803 823 assert_include '1,"Related to #7, Related to #8, Blocks #11"', lines
804 824 assert_include '2,Blocked by #12', lines
805 825 assert_include '3,""', lines
806 826
807 827 get :index, :set_filter => 1, :c => %w(subject relations), :format => 'pdf'
808 828 assert_response :success
809 829 assert_equal 'application/pdf', response.content_type
810 830 end
811 831
812 832 def test_index_send_html_if_query_is_invalid
813 833 get :index, :f => ['start_date'], :op => {:start_date => '='}
814 834 assert_equal 'text/html', @response.content_type
815 835 assert_template 'index'
816 836 end
817 837
818 838 def test_index_send_nothing_if_query_is_invalid
819 839 get :index, :f => ['start_date'], :op => {:start_date => '='}, :format => 'csv'
820 840 assert_equal 'text/csv', @response.content_type
821 841 assert @response.body.blank?
822 842 end
823 843
824 844 def test_show_by_anonymous
825 845 get :show, :id => 1
826 846 assert_response :success
827 847 assert_template 'show'
828 848 assert_equal Issue.find(1), assigns(:issue)
829 849
830 850 assert_select 'div.issue div.description', :text => /Unable to print recipes/
831 851
832 852 # anonymous role is allowed to add a note
833 853 assert_select 'form#issue-form' do
834 854 assert_select 'fieldset' do
835 855 assert_select 'legend', :text => 'Notes'
836 856 assert_select 'textarea[name=?]', 'issue[notes]'
837 857 end
838 858 end
839 859
840 860 assert_select 'title', :text => "Bug #1: Can&#x27;t print recipes - eCookbook - Redmine"
841 861 end
842 862
843 863 def test_show_by_manager
844 864 @request.session[:user_id] = 2
845 865 get :show, :id => 1
846 866 assert_response :success
847 867
848 868 assert_select 'a', :text => /Quote/
849 869
850 870 assert_select 'form#issue-form' do
851 871 assert_select 'fieldset' do
852 872 assert_select 'legend', :text => 'Change properties'
853 873 assert_select 'input[name=?]', 'issue[subject]'
854 874 end
855 875 assert_select 'fieldset' do
856 876 assert_select 'legend', :text => 'Log time'
857 877 assert_select 'input[name=?]', 'time_entry[hours]'
858 878 end
859 879 assert_select 'fieldset' do
860 880 assert_select 'legend', :text => 'Notes'
861 881 assert_select 'textarea[name=?]', 'issue[notes]'
862 882 end
863 883 end
864 884 end
865 885
866 886 def test_show_should_display_update_form
867 887 @request.session[:user_id] = 2
868 888 get :show, :id => 1
869 889 assert_response :success
870 890
871 891 assert_tag 'form', :attributes => {:id => 'issue-form'}
872 892 assert_tag 'input', :attributes => {:name => 'issue[is_private]'}
873 893 assert_tag 'select', :attributes => {:name => 'issue[project_id]'}
874 894 assert_tag 'select', :attributes => {:name => 'issue[tracker_id]'}
875 895 assert_tag 'input', :attributes => {:name => 'issue[subject]'}
876 896 assert_tag 'textarea', :attributes => {:name => 'issue[description]'}
877 897 assert_tag 'select', :attributes => {:name => 'issue[status_id]'}
878 898 assert_tag 'select', :attributes => {:name => 'issue[priority_id]'}
879 899 assert_tag 'select', :attributes => {:name => 'issue[assigned_to_id]'}
880 900 assert_tag 'select', :attributes => {:name => 'issue[category_id]'}
881 901 assert_tag 'select', :attributes => {:name => 'issue[fixed_version_id]'}
882 902 assert_tag 'input', :attributes => {:name => 'issue[parent_issue_id]'}
883 903 assert_tag 'input', :attributes => {:name => 'issue[start_date]'}
884 904 assert_tag 'input', :attributes => {:name => 'issue[due_date]'}
885 905 assert_tag 'select', :attributes => {:name => 'issue[done_ratio]'}
886 906 assert_tag 'input', :attributes => { :name => 'issue[custom_field_values][2]' }
887 907 assert_no_tag 'input', :attributes => {:name => 'issue[watcher_user_ids][]'}
888 908 assert_tag 'textarea', :attributes => {:name => 'issue[notes]'}
889 909 end
890 910
891 911 def test_show_should_display_update_form_with_minimal_permissions
892 912 Role.find(1).update_attribute :permissions, [:view_issues, :add_issue_notes]
893 913 WorkflowTransition.delete_all :role_id => 1
894 914
895 915 @request.session[:user_id] = 2
896 916 get :show, :id => 1
897 917 assert_response :success
898 918
899 919 assert_tag 'form', :attributes => {:id => 'issue-form'}
900 920 assert_no_tag 'input', :attributes => {:name => 'issue[is_private]'}
901 921 assert_no_tag 'select', :attributes => {:name => 'issue[project_id]'}
902 922 assert_no_tag 'select', :attributes => {:name => 'issue[tracker_id]'}
903 923 assert_no_tag 'input', :attributes => {:name => 'issue[subject]'}
904 924 assert_no_tag 'textarea', :attributes => {:name => 'issue[description]'}
905 925 assert_no_tag 'select', :attributes => {:name => 'issue[status_id]'}
906 926 assert_no_tag 'select', :attributes => {:name => 'issue[priority_id]'}
907 927 assert_no_tag 'select', :attributes => {:name => 'issue[assigned_to_id]'}
908 928 assert_no_tag 'select', :attributes => {:name => 'issue[category_id]'}
909 929 assert_no_tag 'select', :attributes => {:name => 'issue[fixed_version_id]'}
910 930 assert_no_tag 'input', :attributes => {:name => 'issue[parent_issue_id]'}
911 931 assert_no_tag 'input', :attributes => {:name => 'issue[start_date]'}
912 932 assert_no_tag 'input', :attributes => {:name => 'issue[due_date]'}
913 933 assert_no_tag 'select', :attributes => {:name => 'issue[done_ratio]'}
914 934 assert_no_tag 'input', :attributes => { :name => 'issue[custom_field_values][2]' }
915 935 assert_no_tag 'input', :attributes => {:name => 'issue[watcher_user_ids][]'}
916 936 assert_tag 'textarea', :attributes => {:name => 'issue[notes]'}
917 937 end
918 938
919 939 def test_show_should_display_update_form_with_workflow_permissions
920 940 Role.find(1).update_attribute :permissions, [:view_issues, :add_issue_notes]
921 941
922 942 @request.session[:user_id] = 2
923 943 get :show, :id => 1
924 944 assert_response :success
925 945
926 946 assert_tag 'form', :attributes => {:id => 'issue-form'}
927 947 assert_no_tag 'input', :attributes => {:name => 'issue[is_private]'}
928 948 assert_no_tag 'select', :attributes => {:name => 'issue[project_id]'}
929 949 assert_no_tag 'select', :attributes => {:name => 'issue[tracker_id]'}
930 950 assert_no_tag 'input', :attributes => {:name => 'issue[subject]'}
931 951 assert_no_tag 'textarea', :attributes => {:name => 'issue[description]'}
932 952 assert_tag 'select', :attributes => {:name => 'issue[status_id]'}
933 953 assert_no_tag 'select', :attributes => {:name => 'issue[priority_id]'}
934 954 assert_tag 'select', :attributes => {:name => 'issue[assigned_to_id]'}
935 955 assert_no_tag 'select', :attributes => {:name => 'issue[category_id]'}
936 956 assert_tag 'select', :attributes => {:name => 'issue[fixed_version_id]'}
937 957 assert_no_tag 'input', :attributes => {:name => 'issue[parent_issue_id]'}
938 958 assert_no_tag 'input', :attributes => {:name => 'issue[start_date]'}
939 959 assert_no_tag 'input', :attributes => {:name => 'issue[due_date]'}
940 960 assert_tag 'select', :attributes => {:name => 'issue[done_ratio]'}
941 961 assert_no_tag 'input', :attributes => { :name => 'issue[custom_field_values][2]' }
942 962 assert_no_tag 'input', :attributes => {:name => 'issue[watcher_user_ids][]'}
943 963 assert_tag 'textarea', :attributes => {:name => 'issue[notes]'}
944 964 end
945 965
946 966 def test_show_should_not_display_update_form_without_permissions
947 967 Role.find(1).update_attribute :permissions, [:view_issues]
948 968
949 969 @request.session[:user_id] = 2
950 970 get :show, :id => 1
951 971 assert_response :success
952 972
953 973 assert_select 'form#issue-form', 0
954 974 end
955 975
956 976 def test_update_form_should_not_display_inactive_enumerations
957 977 assert !IssuePriority.find(15).active?
958 978
959 979 @request.session[:user_id] = 2
960 980 get :show, :id => 1
961 981 assert_response :success
962 982
963 983 assert_select 'form#issue-form' do
964 984 assert_select 'select[name=?]', 'issue[priority_id]' do
965 985 assert_select 'option[value=4]'
966 986 assert_select 'option[value=15]', 0
967 987 end
968 988 end
969 989 end
970 990
971 991 def test_update_form_should_allow_attachment_upload
972 992 @request.session[:user_id] = 2
973 993 get :show, :id => 1
974 994
975 995 assert_select 'form#issue-form[method=post][enctype=multipart/form-data]' do
976 996 assert_select 'input[type=file][name=?]', 'attachments[1][file]'
977 997 end
978 998 end
979 999
980 1000 def test_show_should_deny_anonymous_access_without_permission
981 1001 Role.anonymous.remove_permission!(:view_issues)
982 1002 get :show, :id => 1
983 1003 assert_response :redirect
984 1004 end
985 1005
986 1006 def test_show_should_deny_anonymous_access_to_private_issue
987 1007 Issue.update_all(["is_private = ?", true], "id = 1")
988 1008 get :show, :id => 1
989 1009 assert_response :redirect
990 1010 end
991 1011
992 1012 def test_show_should_deny_non_member_access_without_permission
993 1013 Role.non_member.remove_permission!(:view_issues)
994 1014 @request.session[:user_id] = 9
995 1015 get :show, :id => 1
996 1016 assert_response 403
997 1017 end
998 1018
999 1019 def test_show_should_deny_non_member_access_to_private_issue
1000 1020 Issue.update_all(["is_private = ?", true], "id = 1")
1001 1021 @request.session[:user_id] = 9
1002 1022 get :show, :id => 1
1003 1023 assert_response 403
1004 1024 end
1005 1025
1006 1026 def test_show_should_deny_member_access_without_permission
1007 1027 Role.find(1).remove_permission!(:view_issues)
1008 1028 @request.session[:user_id] = 2
1009 1029 get :show, :id => 1
1010 1030 assert_response 403
1011 1031 end
1012 1032
1013 1033 def test_show_should_deny_member_access_to_private_issue_without_permission
1014 1034 Issue.update_all(["is_private = ?", true], "id = 1")
1015 1035 @request.session[:user_id] = 3
1016 1036 get :show, :id => 1
1017 1037 assert_response 403
1018 1038 end
1019 1039
1020 1040 def test_show_should_allow_author_access_to_private_issue
1021 1041 Issue.update_all(["is_private = ?, author_id = 3", true], "id = 1")
1022 1042 @request.session[:user_id] = 3
1023 1043 get :show, :id => 1
1024 1044 assert_response :success
1025 1045 end
1026 1046
1027 1047 def test_show_should_allow_assignee_access_to_private_issue
1028 1048 Issue.update_all(["is_private = ?, assigned_to_id = 3", true], "id = 1")
1029 1049 @request.session[:user_id] = 3
1030 1050 get :show, :id => 1
1031 1051 assert_response :success
1032 1052 end
1033 1053
1034 1054 def test_show_should_allow_member_access_to_private_issue_with_permission
1035 1055 Issue.update_all(["is_private = ?", true], "id = 1")
1036 1056 User.find(3).roles_for_project(Project.find(1)).first.update_attribute :issues_visibility, 'all'
1037 1057 @request.session[:user_id] = 3
1038 1058 get :show, :id => 1
1039 1059 assert_response :success
1040 1060 end
1041 1061
1042 1062 def test_show_should_not_disclose_relations_to_invisible_issues
1043 1063 Setting.cross_project_issue_relations = '1'
1044 1064 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(2), :relation_type => 'relates')
1045 1065 # Relation to a private project issue
1046 1066 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(4), :relation_type => 'relates')
1047 1067
1048 1068 get :show, :id => 1
1049 1069 assert_response :success
1050 1070
1051 1071 assert_select 'div#relations' do
1052 1072 assert_select 'a', :text => /#2$/
1053 1073 assert_select 'a', :text => /#4$/, :count => 0
1054 1074 end
1055 1075 end
1056 1076
1057 1077 def test_show_should_list_subtasks
1058 1078 Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :parent_issue_id => 1, :subject => 'Child Issue')
1059 1079
1060 1080 get :show, :id => 1
1061 1081 assert_response :success
1062 1082
1063 1083 assert_select 'div#issue_tree' do
1064 1084 assert_select 'td.subject', :text => /Child Issue/
1065 1085 end
1066 1086 end
1067 1087
1068 1088 def test_show_should_list_parents
1069 1089 issue = Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :parent_issue_id => 1, :subject => 'Child Issue')
1070 1090
1071 1091 get :show, :id => issue.id
1072 1092 assert_response :success
1073 1093
1074 1094 assert_select 'div.subject' do
1075 1095 assert_select 'h3', 'Child Issue'
1076 1096 assert_select 'a[href=/issues/1]'
1077 1097 end
1078 1098 end
1079 1099
1080 1100 def test_show_should_not_display_prev_next_links_without_query_in_session
1081 1101 get :show, :id => 1
1082 1102 assert_response :success
1083 1103 assert_nil assigns(:prev_issue_id)
1084 1104 assert_nil assigns(:next_issue_id)
1085 1105
1086 1106 assert_select 'div.next-prev-links', 0
1087 1107 end
1088 1108
1089 1109 def test_show_should_display_prev_next_links_with_query_in_session
1090 1110 @request.session[:query] = {:filters => {'status_id' => {:values => [''], :operator => 'o'}}, :project_id => nil}
1091 1111 @request.session['issues_index_sort'] = 'id'
1092 1112
1093 1113 with_settings :display_subprojects_issues => '0' do
1094 1114 get :show, :id => 3
1095 1115 end
1096 1116
1097 1117 assert_response :success
1098 1118 # Previous and next issues for all projects
1099 1119 assert_equal 2, assigns(:prev_issue_id)
1100 1120 assert_equal 5, assigns(:next_issue_id)
1101 1121
1102 1122 count = Issue.open.visible.count
1103 1123
1104 1124 assert_select 'div.next-prev-links' do
1105 1125 assert_select 'a[href=/issues/2]', :text => /Previous/
1106 1126 assert_select 'a[href=/issues/5]', :text => /Next/
1107 1127 assert_select 'span.position', :text => "3 of #{count}"
1108 1128 end
1109 1129 end
1110 1130
1111 1131 def test_show_should_display_prev_next_links_with_saved_query_in_session
1112 1132 query = Query.create!(:name => 'test', :is_public => true, :user_id => 1,
1113 1133 :filters => {'status_id' => {:values => ['5'], :operator => '='}},
1114 1134 :sort_criteria => [['id', 'asc']])
1115 1135 @request.session[:query] = {:id => query.id, :project_id => nil}
1116 1136
1117 1137 get :show, :id => 11
1118 1138
1119 1139 assert_response :success
1120 1140 assert_equal query, assigns(:query)
1121 1141 # Previous and next issues for all projects
1122 1142 assert_equal 8, assigns(:prev_issue_id)
1123 1143 assert_equal 12, assigns(:next_issue_id)
1124 1144
1125 1145 assert_select 'div.next-prev-links' do
1126 1146 assert_select 'a[href=/issues/8]', :text => /Previous/
1127 1147 assert_select 'a[href=/issues/12]', :text => /Next/
1128 1148 end
1129 1149 end
1130 1150
1131 1151 def test_show_should_display_prev_next_links_with_query_and_sort_on_association
1132 1152 @request.session[:query] = {:filters => {'status_id' => {:values => [''], :operator => 'o'}}, :project_id => nil}
1133 1153
1134 1154 %w(project tracker status priority author assigned_to category fixed_version).each do |assoc_sort|
1135 1155 @request.session['issues_index_sort'] = assoc_sort
1136 1156
1137 1157 get :show, :id => 3
1138 1158 assert_response :success, "Wrong response status for #{assoc_sort} sort"
1139 1159
1140 1160 assert_select 'div.next-prev-links' do
1141 1161 assert_select 'a', :text => /(Previous|Next)/
1142 1162 end
1143 1163 end
1144 1164 end
1145 1165
1146 1166 def test_show_should_display_prev_next_links_with_project_query_in_session
1147 1167 @request.session[:query] = {:filters => {'status_id' => {:values => [''], :operator => 'o'}}, :project_id => 1}
1148 1168 @request.session['issues_index_sort'] = 'id'
1149 1169
1150 1170 with_settings :display_subprojects_issues => '0' do
1151 1171 get :show, :id => 3
1152 1172 end
1153 1173
1154 1174 assert_response :success
1155 1175 # Previous and next issues inside project
1156 1176 assert_equal 2, assigns(:prev_issue_id)
1157 1177 assert_equal 7, assigns(:next_issue_id)
1158 1178
1159 1179 assert_select 'div.next-prev-links' do
1160 1180 assert_select 'a[href=/issues/2]', :text => /Previous/
1161 1181 assert_select 'a[href=/issues/7]', :text => /Next/
1162 1182 end
1163 1183 end
1164 1184
1165 1185 def test_show_should_not_display_prev_link_for_first_issue
1166 1186 @request.session[:query] = {:filters => {'status_id' => {:values => [''], :operator => 'o'}}, :project_id => 1}
1167 1187 @request.session['issues_index_sort'] = 'id'
1168 1188
1169 1189 with_settings :display_subprojects_issues => '0' do
1170 1190 get :show, :id => 1
1171 1191 end
1172 1192
1173 1193 assert_response :success
1174 1194 assert_nil assigns(:prev_issue_id)
1175 1195 assert_equal 2, assigns(:next_issue_id)
1176 1196
1177 1197 assert_select 'div.next-prev-links' do
1178 1198 assert_select 'a', :text => /Previous/, :count => 0
1179 1199 assert_select 'a[href=/issues/2]', :text => /Next/
1180 1200 end
1181 1201 end
1182 1202
1183 1203 def test_show_should_not_display_prev_next_links_for_issue_not_in_query_results
1184 1204 @request.session[:query] = {:filters => {'status_id' => {:values => [''], :operator => 'c'}}, :project_id => 1}
1185 1205 @request.session['issues_index_sort'] = 'id'
1186 1206
1187 1207 get :show, :id => 1
1188 1208
1189 1209 assert_response :success
1190 1210 assert_nil assigns(:prev_issue_id)
1191 1211 assert_nil assigns(:next_issue_id)
1192 1212
1193 1213 assert_select 'a', :text => /Previous/, :count => 0
1194 1214 assert_select 'a', :text => /Next/, :count => 0
1195 1215 end
1196 1216
1197 1217 def test_show_show_should_display_prev_next_links_with_query_sort_by_user_custom_field
1198 1218 cf = IssueCustomField.create!(:name => 'User', :is_for_all => true, :tracker_ids => [1,2,3], :field_format => 'user')
1199 1219 CustomValue.create!(:custom_field => cf, :customized => Issue.find(1), :value => '2')
1200 1220 CustomValue.create!(:custom_field => cf, :customized => Issue.find(2), :value => '3')
1201 1221 CustomValue.create!(:custom_field => cf, :customized => Issue.find(3), :value => '3')
1202 1222 CustomValue.create!(:custom_field => cf, :customized => Issue.find(5), :value => '')
1203 1223
1204 1224 query = Query.create!(:name => 'test', :is_public => true, :user_id => 1, :filters => {},
1205 1225 :sort_criteria => [["cf_#{cf.id}", 'asc'], ['id', 'asc']])
1206 1226 @request.session[:query] = {:id => query.id, :project_id => nil}
1207 1227
1208 1228 get :show, :id => 3
1209 1229 assert_response :success
1210 1230
1211 1231 assert_equal 2, assigns(:prev_issue_id)
1212 1232 assert_equal 1, assigns(:next_issue_id)
1213 1233
1214 1234 assert_select 'div.next-prev-links' do
1215 1235 assert_select 'a[href=/issues/2]', :text => /Previous/
1216 1236 assert_select 'a[href=/issues/1]', :text => /Next/
1217 1237 end
1218 1238 end
1219 1239
1220 1240 def test_show_should_display_link_to_the_assignee
1221 1241 get :show, :id => 2
1222 1242 assert_response :success
1223 1243 assert_select '.assigned-to' do
1224 1244 assert_select 'a[href=/users/3]'
1225 1245 end
1226 1246 end
1227 1247
1228 1248 def test_show_should_display_visible_changesets_from_other_projects
1229 1249 project = Project.find(2)
1230 1250 issue = project.issues.first
1231 1251 issue.changeset_ids = [102]
1232 1252 issue.save!
1233 1253 # changesets from other projects should be displayed even if repository
1234 1254 # is disabled on issue's project
1235 1255 project.disable_module! :repository
1236 1256
1237 1257 @request.session[:user_id] = 2
1238 1258 get :show, :id => issue.id
1239 1259
1240 1260 assert_select 'a[href=?]', '/projects/ecookbook/repository/revisions/3'
1241 1261 end
1242 1262
1243 1263 def test_show_should_display_watchers
1244 1264 @request.session[:user_id] = 2
1245 1265 Issue.find(1).add_watcher User.find(2)
1246 1266
1247 1267 get :show, :id => 1
1248 1268 assert_select 'div#watchers ul' do
1249 1269 assert_select 'li' do
1250 1270 assert_select 'a[href=/users/2]'
1251 1271 assert_select 'a img[alt=Delete]'
1252 1272 end
1253 1273 end
1254 1274 end
1255 1275
1256 1276 def test_show_should_display_watchers_with_gravatars
1257 1277 @request.session[:user_id] = 2
1258 1278 Issue.find(1).add_watcher User.find(2)
1259 1279
1260 1280 with_settings :gravatar_enabled => '1' do
1261 1281 get :show, :id => 1
1262 1282 end
1263 1283
1264 1284 assert_select 'div#watchers ul' do
1265 1285 assert_select 'li' do
1266 1286 assert_select 'img.gravatar'
1267 1287 assert_select 'a[href=/users/2]'
1268 1288 assert_select 'a img[alt=Delete]'
1269 1289 end
1270 1290 end
1271 1291 end
1272 1292
1273 1293 def test_show_with_thumbnails_enabled_should_display_thumbnails
1274 1294 @request.session[:user_id] = 2
1275 1295
1276 1296 with_settings :thumbnails_enabled => '1' do
1277 1297 get :show, :id => 14
1278 1298 assert_response :success
1279 1299 end
1280 1300
1281 1301 assert_select 'div.thumbnails' do
1282 1302 assert_select 'a[href=/attachments/16/testfile.png]' do
1283 1303 assert_select 'img[src=/attachments/thumbnail/16]'
1284 1304 end
1285 1305 end
1286 1306 end
1287 1307
1288 1308 def test_show_with_thumbnails_disabled_should_not_display_thumbnails
1289 1309 @request.session[:user_id] = 2
1290 1310
1291 1311 with_settings :thumbnails_enabled => '0' do
1292 1312 get :show, :id => 14
1293 1313 assert_response :success
1294 1314 end
1295 1315
1296 1316 assert_select 'div.thumbnails', 0
1297 1317 end
1298 1318
1299 1319 def test_show_with_multi_custom_field
1300 1320 field = CustomField.find(1)
1301 1321 field.update_attribute :multiple, true
1302 1322 issue = Issue.find(1)
1303 1323 issue.custom_field_values = {1 => ['MySQL', 'Oracle']}
1304 1324 issue.save!
1305 1325
1306 1326 get :show, :id => 1
1307 1327 assert_response :success
1308 1328
1309 1329 assert_select 'td', :text => 'MySQL, Oracle'
1310 1330 end
1311 1331
1312 1332 def test_show_with_multi_user_custom_field
1313 1333 field = IssueCustomField.create!(:name => 'Multi user', :field_format => 'user', :multiple => true,
1314 1334 :tracker_ids => [1], :is_for_all => true)
1315 1335 issue = Issue.find(1)
1316 1336 issue.custom_field_values = {field.id => ['2', '3']}
1317 1337 issue.save!
1318 1338
1319 1339 get :show, :id => 1
1320 1340 assert_response :success
1321 1341
1322 1342 # TODO: should display links
1323 1343 assert_select 'td', :text => 'Dave Lopper, John Smith'
1324 1344 end
1325 1345
1326 1346 def test_show_should_display_private_notes_with_permission_only
1327 1347 journal = Journal.create!(:journalized => Issue.find(2), :notes => 'Privates notes', :private_notes => true, :user_id => 1)
1328 1348 @request.session[:user_id] = 2
1329 1349
1330 1350 get :show, :id => 2
1331 1351 assert_response :success
1332 1352 assert_include journal, assigns(:journals)
1333 1353
1334 1354 Role.find(1).remove_permission! :view_private_notes
1335 1355 get :show, :id => 2
1336 1356 assert_response :success
1337 1357 assert_not_include journal, assigns(:journals)
1338 1358 end
1339 1359
1340 1360 def test_show_atom
1341 1361 get :show, :id => 2, :format => 'atom'
1342 1362 assert_response :success
1343 1363 assert_template 'journals/index'
1344 1364 # Inline image
1345 1365 assert_select 'content', :text => Regexp.new(Regexp.quote('http://test.host/attachments/download/10'))
1346 1366 end
1347 1367
1348 1368 def test_show_export_to_pdf
1349 1369 get :show, :id => 3, :format => 'pdf'
1350 1370 assert_response :success
1351 1371 assert_equal 'application/pdf', @response.content_type
1352 1372 assert @response.body.starts_with?('%PDF')
1353 1373 assert_not_nil assigns(:issue)
1354 1374 end
1355 1375
1356 1376 def test_show_export_to_pdf_with_ancestors
1357 1377 issue = Issue.generate!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'child', :parent_issue_id => 1)
1358 1378
1359 1379 get :show, :id => issue.id, :format => 'pdf'
1360 1380 assert_response :success
1361 1381 assert_equal 'application/pdf', @response.content_type
1362 1382 assert @response.body.starts_with?('%PDF')
1363 1383 end
1364 1384
1365 1385 def test_show_export_to_pdf_with_descendants
1366 1386 c1 = Issue.generate!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'child', :parent_issue_id => 1)
1367 1387 c2 = Issue.generate!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'child', :parent_issue_id => 1)
1368 1388 c3 = Issue.generate!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'child', :parent_issue_id => c1.id)
1369 1389
1370 1390 get :show, :id => 1, :format => 'pdf'
1371 1391 assert_response :success
1372 1392 assert_equal 'application/pdf', @response.content_type
1373 1393 assert @response.body.starts_with?('%PDF')
1374 1394 end
1375 1395
1376 1396 def test_show_export_to_pdf_with_journals
1377 1397 get :show, :id => 1, :format => 'pdf'
1378 1398 assert_response :success
1379 1399 assert_equal 'application/pdf', @response.content_type
1380 1400 assert @response.body.starts_with?('%PDF')
1381 1401 end
1382 1402
1383 1403 def test_show_export_to_pdf_with_changesets
1384 1404 Issue.find(3).changesets = Changeset.find_all_by_id(100, 101, 102)
1385 1405
1386 1406 get :show, :id => 3, :format => 'pdf'
1387 1407 assert_response :success
1388 1408 assert_equal 'application/pdf', @response.content_type
1389 1409 assert @response.body.starts_with?('%PDF')
1390 1410 end
1391 1411
1392 1412 def test_get_new
1393 1413 @request.session[:user_id] = 2
1394 1414 get :new, :project_id => 1, :tracker_id => 1
1395 1415 assert_response :success
1396 1416 assert_template 'new'
1397 1417
1398 1418 assert_tag 'input', :attributes => {:name => 'issue[is_private]'}
1399 1419 assert_no_tag 'select', :attributes => {:name => 'issue[project_id]'}
1400 1420 assert_tag 'select', :attributes => {:name => 'issue[tracker_id]'}
1401 1421 assert_tag 'input', :attributes => {:name => 'issue[subject]'}
1402 1422 assert_tag 'textarea', :attributes => {:name => 'issue[description]'}
1403 1423 assert_tag 'select', :attributes => {:name => 'issue[status_id]'}
1404 1424 assert_tag 'select', :attributes => {:name => 'issue[priority_id]'}
1405 1425 assert_tag 'select', :attributes => {:name => 'issue[assigned_to_id]'}
1406 1426 assert_tag 'select', :attributes => {:name => 'issue[category_id]'}
1407 1427 assert_tag 'select', :attributes => {:name => 'issue[fixed_version_id]'}
1408 1428 assert_tag 'input', :attributes => {:name => 'issue[parent_issue_id]'}
1409 1429 assert_tag 'input', :attributes => {:name => 'issue[start_date]'}
1410 1430 assert_tag 'input', :attributes => {:name => 'issue[due_date]'}
1411 1431 assert_tag 'select', :attributes => {:name => 'issue[done_ratio]'}
1412 1432 assert_tag 'input', :attributes => { :name => 'issue[custom_field_values][2]', :value => 'Default string' }
1413 1433 assert_tag 'input', :attributes => {:name => 'issue[watcher_user_ids][]'}
1414 1434
1415 1435 # Be sure we don't display inactive IssuePriorities
1416 1436 assert ! IssuePriority.find(15).active?
1417 1437 assert_no_tag :option, :attributes => {:value => '15'},
1418 1438 :parent => {:tag => 'select', :attributes => {:id => 'issue_priority_id'} }
1419 1439 end
1420 1440
1421 1441 def test_get_new_with_minimal_permissions
1422 1442 Role.find(1).update_attribute :permissions, [:add_issues]
1423 1443 WorkflowTransition.delete_all :role_id => 1
1424 1444
1425 1445 @request.session[:user_id] = 2
1426 1446 get :new, :project_id => 1, :tracker_id => 1
1427 1447 assert_response :success
1428 1448 assert_template 'new'
1429 1449
1430 1450 assert_no_tag 'input', :attributes => {:name => 'issue[is_private]'}
1431 1451 assert_no_tag 'select', :attributes => {:name => 'issue[project_id]'}
1432 1452 assert_tag 'select', :attributes => {:name => 'issue[tracker_id]'}
1433 1453 assert_tag 'input', :attributes => {:name => 'issue[subject]'}
1434 1454 assert_tag 'textarea', :attributes => {:name => 'issue[description]'}
1435 1455 assert_tag 'select', :attributes => {:name => 'issue[status_id]'}
1436 1456 assert_tag 'select', :attributes => {:name => 'issue[priority_id]'}
1437 1457 assert_tag 'select', :attributes => {:name => 'issue[assigned_to_id]'}
1438 1458 assert_tag 'select', :attributes => {:name => 'issue[category_id]'}
1439 1459 assert_tag 'select', :attributes => {:name => 'issue[fixed_version_id]'}
1440 1460 assert_no_tag 'input', :attributes => {:name => 'issue[parent_issue_id]'}
1441 1461 assert_tag 'input', :attributes => {:name => 'issue[start_date]'}
1442 1462 assert_tag 'input', :attributes => {:name => 'issue[due_date]'}
1443 1463 assert_tag 'select', :attributes => {:name => 'issue[done_ratio]'}
1444 1464 assert_tag 'input', :attributes => { :name => 'issue[custom_field_values][2]', :value => 'Default string' }
1445 1465 assert_no_tag 'input', :attributes => {:name => 'issue[watcher_user_ids][]'}
1446 1466 end
1447 1467
1448 1468 def test_get_new_with_list_custom_field
1449 1469 @request.session[:user_id] = 2
1450 1470 get :new, :project_id => 1, :tracker_id => 1
1451 1471 assert_response :success
1452 1472 assert_template 'new'
1453 1473
1454 1474 assert_select 'select.list_cf[name=?]', 'issue[custom_field_values][1]' do
1455 1475 assert_select 'option', 4
1456 1476 assert_select 'option[value=MySQL]', :text => 'MySQL'
1457 1477 end
1458 1478 end
1459 1479
1460 1480 def test_get_new_with_multi_custom_field
1461 1481 field = IssueCustomField.find(1)
1462 1482 field.update_attribute :multiple, true
1463 1483
1464 1484 @request.session[:user_id] = 2
1465 1485 get :new, :project_id => 1, :tracker_id => 1
1466 1486 assert_response :success
1467 1487 assert_template 'new'
1468 1488
1469 1489 assert_select 'select[name=?][multiple=multiple]', 'issue[custom_field_values][1][]' do
1470 1490 assert_select 'option', 3
1471 1491 assert_select 'option[value=MySQL]', :text => 'MySQL'
1472 1492 end
1473 1493 assert_select 'input[name=?][type=hidden][value=?]', 'issue[custom_field_values][1][]', ''
1474 1494 end
1475 1495
1476 1496 def test_get_new_with_multi_user_custom_field
1477 1497 field = IssueCustomField.create!(:name => 'Multi user', :field_format => 'user', :multiple => true,
1478 1498 :tracker_ids => [1], :is_for_all => true)
1479 1499
1480 1500 @request.session[:user_id] = 2
1481 1501 get :new, :project_id => 1, :tracker_id => 1
1482 1502 assert_response :success
1483 1503 assert_template 'new'
1484 1504
1485 1505 assert_select 'select[name=?][multiple=multiple]', "issue[custom_field_values][#{field.id}][]" do
1486 1506 assert_select 'option', Project.find(1).users.count
1487 1507 assert_select 'option[value=2]', :text => 'John Smith'
1488 1508 end
1489 1509 assert_select 'input[name=?][type=hidden][value=?]', "issue[custom_field_values][#{field.id}][]", ''
1490 1510 end
1491 1511
1492 1512 def test_get_new_with_date_custom_field
1493 1513 field = IssueCustomField.create!(:name => 'Date', :field_format => 'date', :tracker_ids => [1], :is_for_all => true)
1494 1514
1495 1515 @request.session[:user_id] = 2
1496 1516 get :new, :project_id => 1, :tracker_id => 1
1497 1517 assert_response :success
1498 1518
1499 1519 assert_select 'input[name=?]', "issue[custom_field_values][#{field.id}]"
1500 1520 end
1501 1521
1502 1522 def test_get_new_with_text_custom_field
1503 1523 field = IssueCustomField.create!(:name => 'Text', :field_format => 'text', :tracker_ids => [1], :is_for_all => true)
1504 1524
1505 1525 @request.session[:user_id] = 2
1506 1526 get :new, :project_id => 1, :tracker_id => 1
1507 1527 assert_response :success
1508 1528
1509 1529 assert_select 'textarea[name=?]', "issue[custom_field_values][#{field.id}]"
1510 1530 end
1511 1531
1512 1532 def test_get_new_without_default_start_date_is_creation_date
1513 1533 Setting.default_issue_start_date_to_creation_date = 0
1514 1534
1515 1535 @request.session[:user_id] = 2
1516 1536 get :new, :project_id => 1, :tracker_id => 1
1517 1537 assert_response :success
1518 1538 assert_template 'new'
1519 1539
1520 1540 assert_select 'input[name=?]', 'issue[start_date]'
1521 1541 assert_select 'input[name=?][value]', 'issue[start_date]', 0
1522 1542 end
1523 1543
1524 1544 def test_get_new_with_default_start_date_is_creation_date
1525 1545 Setting.default_issue_start_date_to_creation_date = 1
1526 1546
1527 1547 @request.session[:user_id] = 2
1528 1548 get :new, :project_id => 1, :tracker_id => 1
1529 1549 assert_response :success
1530 1550 assert_template 'new'
1531 1551
1532 1552 assert_select 'input[name=?][value=?]', 'issue[start_date]', Date.today.to_s
1533 1553 end
1534 1554
1535 1555 def test_get_new_form_should_allow_attachment_upload
1536 1556 @request.session[:user_id] = 2
1537 1557 get :new, :project_id => 1, :tracker_id => 1
1538 1558
1539 1559 assert_select 'form[id=issue-form][method=post][enctype=multipart/form-data]' do
1540 1560 assert_select 'input[name=?][type=file]', 'attachments[1][file]'
1541 1561 assert_select 'input[name=?][maxlength=255]', 'attachments[1][description]'
1542 1562 end
1543 1563 end
1544 1564
1545 1565 def test_get_new_should_prefill_the_form_from_params
1546 1566 @request.session[:user_id] = 2
1547 1567 get :new, :project_id => 1,
1548 1568 :issue => {:tracker_id => 3, :description => 'Prefilled', :custom_field_values => {'2' => 'Custom field value'}}
1549 1569
1550 1570 issue = assigns(:issue)
1551 1571 assert_equal 3, issue.tracker_id
1552 1572 assert_equal 'Prefilled', issue.description
1553 1573 assert_equal 'Custom field value', issue.custom_field_value(2)
1554 1574
1555 1575 assert_select 'select[name=?]', 'issue[tracker_id]' do
1556 1576 assert_select 'option[value=3][selected=selected]'
1557 1577 end
1558 1578 assert_select 'textarea[name=?]', 'issue[description]', :text => /Prefilled/
1559 1579 assert_select 'input[name=?][value=?]', 'issue[custom_field_values][2]', 'Custom field value'
1560 1580 end
1561 1581
1562 1582 def test_get_new_should_mark_required_fields
1563 1583 cf1 = IssueCustomField.create!(:name => 'Foo', :field_format => 'string', :is_for_all => true, :tracker_ids => [1, 2])
1564 1584 cf2 = IssueCustomField.create!(:name => 'Bar', :field_format => 'string', :is_for_all => true, :tracker_ids => [1, 2])
1565 1585 WorkflowPermission.delete_all
1566 1586 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => 'due_date', :rule => 'required')
1567 1587 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => cf2.id.to_s, :rule => 'required')
1568 1588 @request.session[:user_id] = 2
1569 1589
1570 1590 get :new, :project_id => 1
1571 1591 assert_response :success
1572 1592 assert_template 'new'
1573 1593
1574 1594 assert_select 'label[for=issue_start_date]' do
1575 1595 assert_select 'span[class=required]', 0
1576 1596 end
1577 1597 assert_select 'label[for=issue_due_date]' do
1578 1598 assert_select 'span[class=required]'
1579 1599 end
1580 1600 assert_select 'label[for=?]', "issue_custom_field_values_#{cf1.id}" do
1581 1601 assert_select 'span[class=required]', 0
1582 1602 end
1583 1603 assert_select 'label[for=?]', "issue_custom_field_values_#{cf2.id}" do
1584 1604 assert_select 'span[class=required]'
1585 1605 end
1586 1606 end
1587 1607
1588 1608 def test_get_new_should_not_display_readonly_fields
1589 1609 cf1 = IssueCustomField.create!(:name => 'Foo', :field_format => 'string', :is_for_all => true, :tracker_ids => [1, 2])
1590 1610 cf2 = IssueCustomField.create!(:name => 'Bar', :field_format => 'string', :is_for_all => true, :tracker_ids => [1, 2])
1591 1611 WorkflowPermission.delete_all
1592 1612 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => 'due_date', :rule => 'readonly')
1593 1613 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => cf2.id.to_s, :rule => 'readonly')
1594 1614 @request.session[:user_id] = 2
1595 1615
1596 1616 get :new, :project_id => 1
1597 1617 assert_response :success
1598 1618 assert_template 'new'
1599 1619
1600 1620 assert_select 'input[name=?]', 'issue[start_date]'
1601 1621 assert_select 'input[name=?]', 'issue[due_date]', 0
1602 1622 assert_select 'input[name=?]', "issue[custom_field_values][#{cf1.id}]"
1603 1623 assert_select 'input[name=?]', "issue[custom_field_values][#{cf2.id}]", 0
1604 1624 end
1605 1625
1606 1626 def test_get_new_without_tracker_id
1607 1627 @request.session[:user_id] = 2
1608 1628 get :new, :project_id => 1
1609 1629 assert_response :success
1610 1630 assert_template 'new'
1611 1631
1612 1632 issue = assigns(:issue)
1613 1633 assert_not_nil issue
1614 1634 assert_equal Project.find(1).trackers.first, issue.tracker
1615 1635 end
1616 1636
1617 1637 def test_get_new_with_no_default_status_should_display_an_error
1618 1638 @request.session[:user_id] = 2
1619 1639 IssueStatus.delete_all
1620 1640
1621 1641 get :new, :project_id => 1
1622 1642 assert_response 500
1623 1643 assert_error_tag :content => /No default issue/
1624 1644 end
1625 1645
1626 1646 def test_get_new_with_no_tracker_should_display_an_error
1627 1647 @request.session[:user_id] = 2
1628 1648 Tracker.delete_all
1629 1649
1630 1650 get :new, :project_id => 1
1631 1651 assert_response 500
1632 1652 assert_error_tag :content => /No tracker/
1633 1653 end
1634 1654
1635 1655 def test_update_new_form
1636 1656 @request.session[:user_id] = 2
1637 1657 xhr :post, :new, :project_id => 1,
1638 1658 :issue => {:tracker_id => 2,
1639 1659 :subject => 'This is the test_new issue',
1640 1660 :description => 'This is the description',
1641 1661 :priority_id => 5}
1642 1662 assert_response :success
1643 1663 assert_template 'update_form'
1644 1664 assert_template 'form'
1645 1665 assert_equal 'text/javascript', response.content_type
1646 1666
1647 1667 issue = assigns(:issue)
1648 1668 assert_kind_of Issue, issue
1649 1669 assert_equal 1, issue.project_id
1650 1670 assert_equal 2, issue.tracker_id
1651 1671 assert_equal 'This is the test_new issue', issue.subject
1652 1672 end
1653 1673
1654 1674 def test_update_new_form_should_propose_transitions_based_on_initial_status
1655 1675 @request.session[:user_id] = 2
1656 1676 WorkflowTransition.delete_all
1657 1677 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 2)
1658 1678 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 5)
1659 1679 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 5, :new_status_id => 4)
1660 1680
1661 1681 xhr :post, :new, :project_id => 1,
1662 1682 :issue => {:tracker_id => 1,
1663 1683 :status_id => 5,
1664 1684 :subject => 'This is an issue'}
1665 1685
1666 1686 assert_equal 5, assigns(:issue).status_id
1667 1687 assert_equal [1,2,5], assigns(:allowed_statuses).map(&:id).sort
1668 1688 end
1669 1689
1670 1690 def test_post_create
1671 1691 @request.session[:user_id] = 2
1672 1692 assert_difference 'Issue.count' do
1673 1693 post :create, :project_id => 1,
1674 1694 :issue => {:tracker_id => 3,
1675 1695 :status_id => 2,
1676 1696 :subject => 'This is the test_new issue',
1677 1697 :description => 'This is the description',
1678 1698 :priority_id => 5,
1679 1699 :start_date => '2010-11-07',
1680 1700 :estimated_hours => '',
1681 1701 :custom_field_values => {'2' => 'Value for field 2'}}
1682 1702 end
1683 1703 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
1684 1704
1685 1705 issue = Issue.find_by_subject('This is the test_new issue')
1686 1706 assert_not_nil issue
1687 1707 assert_equal 2, issue.author_id
1688 1708 assert_equal 3, issue.tracker_id
1689 1709 assert_equal 2, issue.status_id
1690 1710 assert_equal Date.parse('2010-11-07'), issue.start_date
1691 1711 assert_nil issue.estimated_hours
1692 1712 v = issue.custom_values.find(:first, :conditions => {:custom_field_id => 2})
1693 1713 assert_not_nil v
1694 1714 assert_equal 'Value for field 2', v.value
1695 1715 end
1696 1716
1697 1717 def test_post_new_with_group_assignment
1698 1718 group = Group.find(11)
1699 1719 project = Project.find(1)
1700 1720 project.members << Member.new(:principal => group, :roles => [Role.givable.first])
1701 1721
1702 1722 with_settings :issue_group_assignment => '1' do
1703 1723 @request.session[:user_id] = 2
1704 1724 assert_difference 'Issue.count' do
1705 1725 post :create, :project_id => project.id,
1706 1726 :issue => {:tracker_id => 3,
1707 1727 :status_id => 1,
1708 1728 :subject => 'This is the test_new_with_group_assignment issue',
1709 1729 :assigned_to_id => group.id}
1710 1730 end
1711 1731 end
1712 1732 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
1713 1733
1714 1734 issue = Issue.find_by_subject('This is the test_new_with_group_assignment issue')
1715 1735 assert_not_nil issue
1716 1736 assert_equal group, issue.assigned_to
1717 1737 end
1718 1738
1719 1739 def test_post_create_without_start_date_and_default_start_date_is_not_creation_date
1720 1740 Setting.default_issue_start_date_to_creation_date = 0
1721 1741
1722 1742 @request.session[:user_id] = 2
1723 1743 assert_difference 'Issue.count' do
1724 1744 post :create, :project_id => 1,
1725 1745 :issue => {:tracker_id => 3,
1726 1746 :status_id => 2,
1727 1747 :subject => 'This is the test_new issue',
1728 1748 :description => 'This is the description',
1729 1749 :priority_id => 5,
1730 1750 :estimated_hours => '',
1731 1751 :custom_field_values => {'2' => 'Value for field 2'}}
1732 1752 end
1733 1753 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
1734 1754
1735 1755 issue = Issue.find_by_subject('This is the test_new issue')
1736 1756 assert_not_nil issue
1737 1757 assert_nil issue.start_date
1738 1758 end
1739 1759
1740 1760 def test_post_create_without_start_date_and_default_start_date_is_creation_date
1741 1761 Setting.default_issue_start_date_to_creation_date = 1
1742 1762
1743 1763 @request.session[:user_id] = 2
1744 1764 assert_difference 'Issue.count' do
1745 1765 post :create, :project_id => 1,
1746 1766 :issue => {:tracker_id => 3,
1747 1767 :status_id => 2,
1748 1768 :subject => 'This is the test_new issue',
1749 1769 :description => 'This is the description',
1750 1770 :priority_id => 5,
1751 1771 :estimated_hours => '',
1752 1772 :custom_field_values => {'2' => 'Value for field 2'}}
1753 1773 end
1754 1774 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
1755 1775
1756 1776 issue = Issue.find_by_subject('This is the test_new issue')
1757 1777 assert_not_nil issue
1758 1778 assert_equal Date.today, issue.start_date
1759 1779 end
1760 1780
1761 1781 def test_post_create_and_continue
1762 1782 @request.session[:user_id] = 2
1763 1783 assert_difference 'Issue.count' do
1764 1784 post :create, :project_id => 1,
1765 1785 :issue => {:tracker_id => 3, :subject => 'This is first issue', :priority_id => 5},
1766 1786 :continue => ''
1767 1787 end
1768 1788
1769 1789 issue = Issue.first(:order => 'id DESC')
1770 1790 assert_redirected_to :controller => 'issues', :action => 'new', :project_id => 'ecookbook', :issue => {:tracker_id => 3}
1771 1791 assert_not_nil flash[:notice], "flash was not set"
1772 1792 assert_include %|<a href="/issues/#{issue.id}" title="This is first issue">##{issue.id}</a>|, flash[:notice], "issue link not found in the flash message"
1773 1793 end
1774 1794
1775 1795 def test_post_create_without_custom_fields_param
1776 1796 @request.session[:user_id] = 2
1777 1797 assert_difference 'Issue.count' do
1778 1798 post :create, :project_id => 1,
1779 1799 :issue => {:tracker_id => 1,
1780 1800 :subject => 'This is the test_new issue',
1781 1801 :description => 'This is the description',
1782 1802 :priority_id => 5}
1783 1803 end
1784 1804 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
1785 1805 end
1786 1806
1787 1807 def test_post_create_with_multi_custom_field
1788 1808 field = IssueCustomField.find_by_name('Database')
1789 1809 field.update_attribute(:multiple, true)
1790 1810
1791 1811 @request.session[:user_id] = 2
1792 1812 assert_difference 'Issue.count' do
1793 1813 post :create, :project_id => 1,
1794 1814 :issue => {:tracker_id => 1,
1795 1815 :subject => 'This is the test_new issue',
1796 1816 :description => 'This is the description',
1797 1817 :priority_id => 5,
1798 1818 :custom_field_values => {'1' => ['', 'MySQL', 'Oracle']}}
1799 1819 end
1800 1820 assert_response 302
1801 1821 issue = Issue.first(:order => 'id DESC')
1802 1822 assert_equal ['MySQL', 'Oracle'], issue.custom_field_value(1).sort
1803 1823 end
1804 1824
1805 1825 def test_post_create_with_empty_multi_custom_field
1806 1826 field = IssueCustomField.find_by_name('Database')
1807 1827 field.update_attribute(:multiple, true)
1808 1828
1809 1829 @request.session[:user_id] = 2
1810 1830 assert_difference 'Issue.count' do
1811 1831 post :create, :project_id => 1,
1812 1832 :issue => {:tracker_id => 1,
1813 1833 :subject => 'This is the test_new issue',
1814 1834 :description => 'This is the description',
1815 1835 :priority_id => 5,
1816 1836 :custom_field_values => {'1' => ['']}}
1817 1837 end
1818 1838 assert_response 302
1819 1839 issue = Issue.first(:order => 'id DESC')
1820 1840 assert_equal [''], issue.custom_field_value(1).sort
1821 1841 end
1822 1842
1823 1843 def test_post_create_with_multi_user_custom_field
1824 1844 field = IssueCustomField.create!(:name => 'Multi user', :field_format => 'user', :multiple => true,
1825 1845 :tracker_ids => [1], :is_for_all => true)
1826 1846
1827 1847 @request.session[:user_id] = 2
1828 1848 assert_difference 'Issue.count' do
1829 1849 post :create, :project_id => 1,
1830 1850 :issue => {:tracker_id => 1,
1831 1851 :subject => 'This is the test_new issue',
1832 1852 :description => 'This is the description',
1833 1853 :priority_id => 5,
1834 1854 :custom_field_values => {field.id.to_s => ['', '2', '3']}}
1835 1855 end
1836 1856 assert_response 302
1837 1857 issue = Issue.first(:order => 'id DESC')
1838 1858 assert_equal ['2', '3'], issue.custom_field_value(field).sort
1839 1859 end
1840 1860
1841 1861 def test_post_create_with_required_custom_field_and_without_custom_fields_param
1842 1862 field = IssueCustomField.find_by_name('Database')
1843 1863 field.update_attribute(:is_required, true)
1844 1864
1845 1865 @request.session[:user_id] = 2
1846 1866 assert_no_difference 'Issue.count' do
1847 1867 post :create, :project_id => 1,
1848 1868 :issue => {:tracker_id => 1,
1849 1869 :subject => 'This is the test_new issue',
1850 1870 :description => 'This is the description',
1851 1871 :priority_id => 5}
1852 1872 end
1853 1873 assert_response :success
1854 1874 assert_template 'new'
1855 1875 issue = assigns(:issue)
1856 1876 assert_not_nil issue
1857 1877 assert_error_tag :content => /Database can&#x27;t be blank/
1858 1878 end
1859 1879
1860 1880 def test_create_should_validate_required_fields
1861 1881 cf1 = IssueCustomField.create!(:name => 'Foo', :field_format => 'string', :is_for_all => true, :tracker_ids => [1, 2])
1862 1882 cf2 = IssueCustomField.create!(:name => 'Bar', :field_format => 'string', :is_for_all => true, :tracker_ids => [1, 2])
1863 1883 WorkflowPermission.delete_all
1864 1884 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2, :role_id => 1, :field_name => 'due_date', :rule => 'required')
1865 1885 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2, :role_id => 1, :field_name => cf2.id.to_s, :rule => 'required')
1866 1886 @request.session[:user_id] = 2
1867 1887
1868 1888 assert_no_difference 'Issue.count' do
1869 1889 post :create, :project_id => 1, :issue => {
1870 1890 :tracker_id => 2,
1871 1891 :status_id => 1,
1872 1892 :subject => 'Test',
1873 1893 :start_date => '',
1874 1894 :due_date => '',
1875 1895 :custom_field_values => {cf1.id.to_s => '', cf2.id.to_s => ''}
1876 1896 }
1877 1897 assert_response :success
1878 1898 assert_template 'new'
1879 1899 end
1880 1900
1881 1901 assert_error_tag :content => /Due date can&#x27;t be blank/i
1882 1902 assert_error_tag :content => /Bar can&#x27;t be blank/i
1883 1903 end
1884 1904
1885 1905 def test_create_should_ignore_readonly_fields
1886 1906 cf1 = IssueCustomField.create!(:name => 'Foo', :field_format => 'string', :is_for_all => true, :tracker_ids => [1, 2])
1887 1907 cf2 = IssueCustomField.create!(:name => 'Bar', :field_format => 'string', :is_for_all => true, :tracker_ids => [1, 2])
1888 1908 WorkflowPermission.delete_all
1889 1909 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2, :role_id => 1, :field_name => 'due_date', :rule => 'readonly')
1890 1910 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2, :role_id => 1, :field_name => cf2.id.to_s, :rule => 'readonly')
1891 1911 @request.session[:user_id] = 2
1892 1912
1893 1913 assert_difference 'Issue.count' do
1894 1914 post :create, :project_id => 1, :issue => {
1895 1915 :tracker_id => 2,
1896 1916 :status_id => 1,
1897 1917 :subject => 'Test',
1898 1918 :start_date => '2012-07-14',
1899 1919 :due_date => '2012-07-16',
1900 1920 :custom_field_values => {cf1.id.to_s => 'value1', cf2.id.to_s => 'value2'}
1901 1921 }
1902 1922 assert_response 302
1903 1923 end
1904 1924
1905 1925 issue = Issue.first(:order => 'id DESC')
1906 1926 assert_equal Date.parse('2012-07-14'), issue.start_date
1907 1927 assert_nil issue.due_date
1908 1928 assert_equal 'value1', issue.custom_field_value(cf1)
1909 1929 assert_nil issue.custom_field_value(cf2)
1910 1930 end
1911 1931
1912 1932 def test_post_create_with_watchers
1913 1933 @request.session[:user_id] = 2
1914 1934 ActionMailer::Base.deliveries.clear
1915 1935
1916 1936 assert_difference 'Watcher.count', 2 do
1917 1937 post :create, :project_id => 1,
1918 1938 :issue => {:tracker_id => 1,
1919 1939 :subject => 'This is a new issue with watchers',
1920 1940 :description => 'This is the description',
1921 1941 :priority_id => 5,
1922 1942 :watcher_user_ids => ['2', '3']}
1923 1943 end
1924 1944 issue = Issue.find_by_subject('This is a new issue with watchers')
1925 1945 assert_not_nil issue
1926 1946 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
1927 1947
1928 1948 # Watchers added
1929 1949 assert_equal [2, 3], issue.watcher_user_ids.sort
1930 1950 assert issue.watched_by?(User.find(3))
1931 1951 # Watchers notified
1932 1952 mail = ActionMailer::Base.deliveries.last
1933 1953 assert_not_nil mail
1934 1954 assert [mail.bcc, mail.cc].flatten.include?(User.find(3).mail)
1935 1955 end
1936 1956
1937 1957 def test_post_create_subissue
1938 1958 @request.session[:user_id] = 2
1939 1959
1940 1960 assert_difference 'Issue.count' do
1941 1961 post :create, :project_id => 1,
1942 1962 :issue => {:tracker_id => 1,
1943 1963 :subject => 'This is a child issue',
1944 1964 :parent_issue_id => '2'}
1945 1965 assert_response 302
1946 1966 end
1947 1967 issue = Issue.order('id DESC').first
1948 1968 assert_equal Issue.find(2), issue.parent
1949 1969 end
1950 1970
1951 1971 def test_post_create_subissue_with_sharp_parent_id
1952 1972 @request.session[:user_id] = 2
1953 1973
1954 1974 assert_difference 'Issue.count' do
1955 1975 post :create, :project_id => 1,
1956 1976 :issue => {:tracker_id => 1,
1957 1977 :subject => 'This is a child issue',
1958 1978 :parent_issue_id => '#2'}
1959 1979 assert_response 302
1960 1980 end
1961 1981 issue = Issue.order('id DESC').first
1962 1982 assert_equal Issue.find(2), issue.parent
1963 1983 end
1964 1984
1965 1985 def test_post_create_subissue_with_non_visible_parent_id_should_not_validate
1966 1986 @request.session[:user_id] = 2
1967 1987
1968 1988 assert_no_difference 'Issue.count' do
1969 1989 post :create, :project_id => 1,
1970 1990 :issue => {:tracker_id => 1,
1971 1991 :subject => 'This is a child issue',
1972 1992 :parent_issue_id => '4'}
1973 1993
1974 1994 assert_response :success
1975 1995 assert_select 'input[name=?][value=?]', 'issue[parent_issue_id]', '4'
1976 1996 assert_error_tag :content => /Parent task is invalid/i
1977 1997 end
1978 1998 end
1979 1999
1980 2000 def test_post_create_subissue_with_non_numeric_parent_id_should_not_validate
1981 2001 @request.session[:user_id] = 2
1982 2002
1983 2003 assert_no_difference 'Issue.count' do
1984 2004 post :create, :project_id => 1,
1985 2005 :issue => {:tracker_id => 1,
1986 2006 :subject => 'This is a child issue',
1987 2007 :parent_issue_id => '01ABC'}
1988 2008
1989 2009 assert_response :success
1990 2010 assert_select 'input[name=?][value=?]', 'issue[parent_issue_id]', '01ABC'
1991 2011 assert_error_tag :content => /Parent task is invalid/i
1992 2012 end
1993 2013 end
1994 2014
1995 2015 def test_post_create_private
1996 2016 @request.session[:user_id] = 2
1997 2017
1998 2018 assert_difference 'Issue.count' do
1999 2019 post :create, :project_id => 1,
2000 2020 :issue => {:tracker_id => 1,
2001 2021 :subject => 'This is a private issue',
2002 2022 :is_private => '1'}
2003 2023 end
2004 2024 issue = Issue.first(:order => 'id DESC')
2005 2025 assert issue.is_private?
2006 2026 end
2007 2027
2008 2028 def test_post_create_private_with_set_own_issues_private_permission
2009 2029 role = Role.find(1)
2010 2030 role.remove_permission! :set_issues_private
2011 2031 role.add_permission! :set_own_issues_private
2012 2032
2013 2033 @request.session[:user_id] = 2
2014 2034
2015 2035 assert_difference 'Issue.count' do
2016 2036 post :create, :project_id => 1,
2017 2037 :issue => {:tracker_id => 1,
2018 2038 :subject => 'This is a private issue',
2019 2039 :is_private => '1'}
2020 2040 end
2021 2041 issue = Issue.first(:order => 'id DESC')
2022 2042 assert issue.is_private?
2023 2043 end
2024 2044
2025 2045 def test_post_create_should_send_a_notification
2026 2046 ActionMailer::Base.deliveries.clear
2027 2047 @request.session[:user_id] = 2
2028 2048 assert_difference 'Issue.count' do
2029 2049 post :create, :project_id => 1,
2030 2050 :issue => {:tracker_id => 3,
2031 2051 :subject => 'This is the test_new issue',
2032 2052 :description => 'This is the description',
2033 2053 :priority_id => 5,
2034 2054 :estimated_hours => '',
2035 2055 :custom_field_values => {'2' => 'Value for field 2'}}
2036 2056 end
2037 2057 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
2038 2058
2039 2059 assert_equal 1, ActionMailer::Base.deliveries.size
2040 2060 end
2041 2061
2042 2062 def test_post_create_should_preserve_fields_values_on_validation_failure
2043 2063 @request.session[:user_id] = 2
2044 2064 post :create, :project_id => 1,
2045 2065 :issue => {:tracker_id => 1,
2046 2066 # empty subject
2047 2067 :subject => '',
2048 2068 :description => 'This is a description',
2049 2069 :priority_id => 6,
2050 2070 :custom_field_values => {'1' => 'Oracle', '2' => 'Value for field 2'}}
2051 2071 assert_response :success
2052 2072 assert_template 'new'
2053 2073
2054 2074 assert_tag :textarea, :attributes => { :name => 'issue[description]' },
2055 2075 :content => "\nThis is a description"
2056 2076 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
2057 2077 :child => { :tag => 'option', :attributes => { :selected => 'selected',
2058 2078 :value => '6' },
2059 2079 :content => 'High' }
2060 2080 # Custom fields
2061 2081 assert_tag :select, :attributes => { :name => 'issue[custom_field_values][1]' },
2062 2082 :child => { :tag => 'option', :attributes => { :selected => 'selected',
2063 2083 :value => 'Oracle' },
2064 2084 :content => 'Oracle' }
2065 2085 assert_tag :input, :attributes => { :name => 'issue[custom_field_values][2]',
2066 2086 :value => 'Value for field 2'}
2067 2087 end
2068 2088
2069 2089 def test_post_create_with_failure_should_preserve_watchers
2070 2090 assert !User.find(8).member_of?(Project.find(1))
2071 2091
2072 2092 @request.session[:user_id] = 2
2073 2093 post :create, :project_id => 1,
2074 2094 :issue => {:tracker_id => 1,
2075 2095 :watcher_user_ids => ['3', '8']}
2076 2096 assert_response :success
2077 2097 assert_template 'new'
2078 2098
2079 2099 assert_tag 'input', :attributes => {:name => 'issue[watcher_user_ids][]', :value => '2', :checked => nil}
2080 2100 assert_tag 'input', :attributes => {:name => 'issue[watcher_user_ids][]', :value => '3', :checked => 'checked'}
2081 2101 assert_tag 'input', :attributes => {:name => 'issue[watcher_user_ids][]', :value => '8', :checked => 'checked'}
2082 2102 end
2083 2103
2084 2104 def test_post_create_should_ignore_non_safe_attributes
2085 2105 @request.session[:user_id] = 2
2086 2106 assert_nothing_raised do
2087 2107 post :create, :project_id => 1, :issue => { :tracker => "A param can not be a Tracker" }
2088 2108 end
2089 2109 end
2090 2110
2091 2111 def test_post_create_with_attachment
2092 2112 set_tmp_attachments_directory
2093 2113 @request.session[:user_id] = 2
2094 2114
2095 2115 assert_difference 'Issue.count' do
2096 2116 assert_difference 'Attachment.count' do
2097 2117 post :create, :project_id => 1,
2098 2118 :issue => { :tracker_id => '1', :subject => 'With attachment' },
2099 2119 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'test file'}}
2100 2120 end
2101 2121 end
2102 2122
2103 2123 issue = Issue.first(:order => 'id DESC')
2104 2124 attachment = Attachment.first(:order => 'id DESC')
2105 2125
2106 2126 assert_equal issue, attachment.container
2107 2127 assert_equal 2, attachment.author_id
2108 2128 assert_equal 'testfile.txt', attachment.filename
2109 2129 assert_equal 'text/plain', attachment.content_type
2110 2130 assert_equal 'test file', attachment.description
2111 2131 assert_equal 59, attachment.filesize
2112 2132 assert File.exists?(attachment.diskfile)
2113 2133 assert_equal 59, File.size(attachment.diskfile)
2114 2134 end
2115 2135
2116 2136 def test_post_create_with_failure_should_save_attachments
2117 2137 set_tmp_attachments_directory
2118 2138 @request.session[:user_id] = 2
2119 2139
2120 2140 assert_no_difference 'Issue.count' do
2121 2141 assert_difference 'Attachment.count' do
2122 2142 post :create, :project_id => 1,
2123 2143 :issue => { :tracker_id => '1', :subject => '' },
2124 2144 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'test file'}}
2125 2145 assert_response :success
2126 2146 assert_template 'new'
2127 2147 end
2128 2148 end
2129 2149
2130 2150 attachment = Attachment.first(:order => 'id DESC')
2131 2151 assert_equal 'testfile.txt', attachment.filename
2132 2152 assert File.exists?(attachment.diskfile)
2133 2153 assert_nil attachment.container
2134 2154
2135 2155 assert_tag 'input', :attributes => {:name => 'attachments[p0][token]', :value => attachment.token}
2136 2156 assert_tag 'span', :content => /testfile.txt/
2137 2157 end
2138 2158
2139 2159 def test_post_create_with_failure_should_keep_saved_attachments
2140 2160 set_tmp_attachments_directory
2141 2161 attachment = Attachment.create!(:file => uploaded_test_file("testfile.txt", "text/plain"), :author_id => 2)
2142 2162 @request.session[:user_id] = 2
2143 2163
2144 2164 assert_no_difference 'Issue.count' do
2145 2165 assert_no_difference 'Attachment.count' do
2146 2166 post :create, :project_id => 1,
2147 2167 :issue => { :tracker_id => '1', :subject => '' },
2148 2168 :attachments => {'p0' => {'token' => attachment.token}}
2149 2169 assert_response :success
2150 2170 assert_template 'new'
2151 2171 end
2152 2172 end
2153 2173
2154 2174 assert_tag 'input', :attributes => {:name => 'attachments[p0][token]', :value => attachment.token}
2155 2175 assert_tag 'span', :content => /testfile.txt/
2156 2176 end
2157 2177
2158 2178 def test_post_create_should_attach_saved_attachments
2159 2179 set_tmp_attachments_directory
2160 2180 attachment = Attachment.create!(:file => uploaded_test_file("testfile.txt", "text/plain"), :author_id => 2)
2161 2181 @request.session[:user_id] = 2
2162 2182
2163 2183 assert_difference 'Issue.count' do
2164 2184 assert_no_difference 'Attachment.count' do
2165 2185 post :create, :project_id => 1,
2166 2186 :issue => { :tracker_id => '1', :subject => 'Saved attachments' },
2167 2187 :attachments => {'p0' => {'token' => attachment.token}}
2168 2188 assert_response 302
2169 2189 end
2170 2190 end
2171 2191
2172 2192 issue = Issue.first(:order => 'id DESC')
2173 2193 assert_equal 1, issue.attachments.count
2174 2194
2175 2195 attachment.reload
2176 2196 assert_equal issue, attachment.container
2177 2197 end
2178 2198
2179 2199 context "without workflow privilege" do
2180 2200 setup do
2181 2201 WorkflowTransition.delete_all(["role_id = ?", Role.anonymous.id])
2182 2202 Role.anonymous.add_permission! :add_issues, :add_issue_notes
2183 2203 end
2184 2204
2185 2205 context "#new" do
2186 2206 should "propose default status only" do
2187 2207 get :new, :project_id => 1
2188 2208 assert_response :success
2189 2209 assert_template 'new'
2190 2210 assert_tag :tag => 'select',
2191 2211 :attributes => {:name => 'issue[status_id]'},
2192 2212 :children => {:count => 1},
2193 2213 :child => {:tag => 'option', :attributes => {:value => IssueStatus.default.id.to_s}}
2194 2214 end
2195 2215
2196 2216 should "accept default status" do
2197 2217 assert_difference 'Issue.count' do
2198 2218 post :create, :project_id => 1,
2199 2219 :issue => {:tracker_id => 1,
2200 2220 :subject => 'This is an issue',
2201 2221 :status_id => 1}
2202 2222 end
2203 2223 issue = Issue.last(:order => 'id')
2204 2224 assert_equal IssueStatus.default, issue.status
2205 2225 end
2206 2226
2207 2227 should "ignore unauthorized status" do
2208 2228 assert_difference 'Issue.count' do
2209 2229 post :create, :project_id => 1,
2210 2230 :issue => {:tracker_id => 1,
2211 2231 :subject => 'This is an issue',
2212 2232 :status_id => 3}
2213 2233 end
2214 2234 issue = Issue.last(:order => 'id')
2215 2235 assert_equal IssueStatus.default, issue.status
2216 2236 end
2217 2237 end
2218 2238
2219 2239 context "#update" do
2220 2240 should "ignore status change" do
2221 2241 assert_difference 'Journal.count' do
2222 2242 put :update, :id => 1, :issue => {:status_id => 3, :notes => 'just trying'}
2223 2243 end
2224 2244 assert_equal 1, Issue.find(1).status_id
2225 2245 end
2226 2246
2227 2247 should "ignore attributes changes" do
2228 2248 assert_difference 'Journal.count' do
2229 2249 put :update, :id => 1, :issue => {:subject => 'changed', :assigned_to_id => 2, :notes => 'just trying'}
2230 2250 end
2231 2251 issue = Issue.find(1)
2232 2252 assert_equal "Can't print recipes", issue.subject
2233 2253 assert_nil issue.assigned_to
2234 2254 end
2235 2255 end
2236 2256 end
2237 2257
2238 2258 context "with workflow privilege" do
2239 2259 setup do
2240 2260 WorkflowTransition.delete_all(["role_id = ?", Role.anonymous.id])
2241 2261 WorkflowTransition.create!(:role => Role.anonymous, :tracker_id => 1, :old_status_id => 1, :new_status_id => 3)
2242 2262 WorkflowTransition.create!(:role => Role.anonymous, :tracker_id => 1, :old_status_id => 1, :new_status_id => 4)
2243 2263 Role.anonymous.add_permission! :add_issues, :add_issue_notes
2244 2264 end
2245 2265
2246 2266 context "#update" do
2247 2267 should "accept authorized status" do
2248 2268 assert_difference 'Journal.count' do
2249 2269 put :update, :id => 1, :issue => {:status_id => 3, :notes => 'just trying'}
2250 2270 end
2251 2271 assert_equal 3, Issue.find(1).status_id
2252 2272 end
2253 2273
2254 2274 should "ignore unauthorized status" do
2255 2275 assert_difference 'Journal.count' do
2256 2276 put :update, :id => 1, :issue => {:status_id => 2, :notes => 'just trying'}
2257 2277 end
2258 2278 assert_equal 1, Issue.find(1).status_id
2259 2279 end
2260 2280
2261 2281 should "accept authorized attributes changes" do
2262 2282 assert_difference 'Journal.count' do
2263 2283 put :update, :id => 1, :issue => {:assigned_to_id => 2, :notes => 'just trying'}
2264 2284 end
2265 2285 issue = Issue.find(1)
2266 2286 assert_equal 2, issue.assigned_to_id
2267 2287 end
2268 2288
2269 2289 should "ignore unauthorized attributes changes" do
2270 2290 assert_difference 'Journal.count' do
2271 2291 put :update, :id => 1, :issue => {:subject => 'changed', :notes => 'just trying'}
2272 2292 end
2273 2293 issue = Issue.find(1)
2274 2294 assert_equal "Can't print recipes", issue.subject
2275 2295 end
2276 2296 end
2277 2297
2278 2298 context "and :edit_issues permission" do
2279 2299 setup do
2280 2300 Role.anonymous.add_permission! :add_issues, :edit_issues
2281 2301 end
2282 2302
2283 2303 should "accept authorized status" do
2284 2304 assert_difference 'Journal.count' do
2285 2305 put :update, :id => 1, :issue => {:status_id => 3, :notes => 'just trying'}
2286 2306 end
2287 2307 assert_equal 3, Issue.find(1).status_id
2288 2308 end
2289 2309
2290 2310 should "ignore unauthorized status" do
2291 2311 assert_difference 'Journal.count' do
2292 2312 put :update, :id => 1, :issue => {:status_id => 2, :notes => 'just trying'}
2293 2313 end
2294 2314 assert_equal 1, Issue.find(1).status_id
2295 2315 end
2296 2316
2297 2317 should "accept authorized attributes changes" do
2298 2318 assert_difference 'Journal.count' do
2299 2319 put :update, :id => 1, :issue => {:subject => 'changed', :assigned_to_id => 2, :notes => 'just trying'}
2300 2320 end
2301 2321 issue = Issue.find(1)
2302 2322 assert_equal "changed", issue.subject
2303 2323 assert_equal 2, issue.assigned_to_id
2304 2324 end
2305 2325 end
2306 2326 end
2307 2327
2308 2328 def test_new_as_copy
2309 2329 @request.session[:user_id] = 2
2310 2330 get :new, :project_id => 1, :copy_from => 1
2311 2331
2312 2332 assert_response :success
2313 2333 assert_template 'new'
2314 2334
2315 2335 assert_not_nil assigns(:issue)
2316 2336 orig = Issue.find(1)
2317 2337 assert_equal 1, assigns(:issue).project_id
2318 2338 assert_equal orig.subject, assigns(:issue).subject
2319 2339 assert assigns(:issue).copy?
2320 2340
2321 2341 assert_tag 'form', :attributes => {:id => 'issue-form', :action => '/projects/ecookbook/issues'}
2322 2342 assert_tag 'select', :attributes => {:name => 'issue[project_id]'}
2323 2343 assert_tag 'select', :attributes => {:name => 'issue[project_id]'},
2324 2344 :child => {:tag => 'option', :attributes => {:value => '1', :selected => 'selected'}, :content => 'eCookbook'}
2325 2345 assert_tag 'select', :attributes => {:name => 'issue[project_id]'},
2326 2346 :child => {:tag => 'option', :attributes => {:value => '2', :selected => nil}, :content => 'OnlineStore'}
2327 2347 assert_tag 'input', :attributes => {:name => 'copy_from', :value => '1'}
2328 2348 end
2329 2349
2330 2350 def test_new_as_copy_with_attachments_should_show_copy_attachments_checkbox
2331 2351 @request.session[:user_id] = 2
2332 2352 issue = Issue.find(3)
2333 2353 assert issue.attachments.count > 0
2334 2354 get :new, :project_id => 1, :copy_from => 3
2335 2355
2336 2356 assert_tag 'input', :attributes => {:name => 'copy_attachments', :type => 'checkbox', :checked => 'checked', :value => '1'}
2337 2357 end
2338 2358
2339 2359 def test_new_as_copy_without_attachments_should_not_show_copy_attachments_checkbox
2340 2360 @request.session[:user_id] = 2
2341 2361 issue = Issue.find(3)
2342 2362 issue.attachments.delete_all
2343 2363 get :new, :project_id => 1, :copy_from => 3
2344 2364
2345 2365 assert_no_tag 'input', :attributes => {:name => 'copy_attachments', :type => 'checkbox', :checked => 'checked', :value => '1'}
2346 2366 end
2347 2367
2348 2368 def test_new_as_copy_with_subtasks_should_show_copy_subtasks_checkbox
2349 2369 @request.session[:user_id] = 2
2350 2370 issue = Issue.generate_with_descendants!
2351 2371 get :new, :project_id => 1, :copy_from => issue.id
2352 2372
2353 2373 assert_select 'input[type=checkbox][name=copy_subtasks][checked=checked][value=1]'
2354 2374 end
2355 2375
2356 2376 def test_new_as_copy_with_invalid_issue_should_respond_with_404
2357 2377 @request.session[:user_id] = 2
2358 2378 get :new, :project_id => 1, :copy_from => 99999
2359 2379 assert_response 404
2360 2380 end
2361 2381
2362 2382 def test_create_as_copy_on_different_project
2363 2383 @request.session[:user_id] = 2
2364 2384 assert_difference 'Issue.count' do
2365 2385 post :create, :project_id => 1, :copy_from => 1,
2366 2386 :issue => {:project_id => '2', :tracker_id => '3', :status_id => '1', :subject => 'Copy'}
2367 2387
2368 2388 assert_not_nil assigns(:issue)
2369 2389 assert assigns(:issue).copy?
2370 2390 end
2371 2391 issue = Issue.first(:order => 'id DESC')
2372 2392 assert_redirected_to "/issues/#{issue.id}"
2373 2393
2374 2394 assert_equal 2, issue.project_id
2375 2395 assert_equal 3, issue.tracker_id
2376 2396 assert_equal 'Copy', issue.subject
2377 2397 end
2378 2398
2379 2399 def test_create_as_copy_should_copy_attachments
2380 2400 @request.session[:user_id] = 2
2381 2401 issue = Issue.find(3)
2382 2402 count = issue.attachments.count
2383 2403 assert count > 0
2384 2404
2385 2405 assert_difference 'Issue.count' do
2386 2406 assert_difference 'Attachment.count', count do
2387 2407 assert_no_difference 'Journal.count' do
2388 2408 post :create, :project_id => 1, :copy_from => 3,
2389 2409 :issue => {:project_id => '1', :tracker_id => '3', :status_id => '1', :subject => 'Copy with attachments'},
2390 2410 :copy_attachments => '1'
2391 2411 end
2392 2412 end
2393 2413 end
2394 2414 copy = Issue.first(:order => 'id DESC')
2395 2415 assert_equal count, copy.attachments.count
2396 2416 assert_equal issue.attachments.map(&:filename).sort, copy.attachments.map(&:filename).sort
2397 2417 end
2398 2418
2399 2419 def test_create_as_copy_without_copy_attachments_option_should_not_copy_attachments
2400 2420 @request.session[:user_id] = 2
2401 2421 issue = Issue.find(3)
2402 2422 count = issue.attachments.count
2403 2423 assert count > 0
2404 2424
2405 2425 assert_difference 'Issue.count' do
2406 2426 assert_no_difference 'Attachment.count' do
2407 2427 assert_no_difference 'Journal.count' do
2408 2428 post :create, :project_id => 1, :copy_from => 3,
2409 2429 :issue => {:project_id => '1', :tracker_id => '3', :status_id => '1', :subject => 'Copy with attachments'}
2410 2430 end
2411 2431 end
2412 2432 end
2413 2433 copy = Issue.first(:order => 'id DESC')
2414 2434 assert_equal 0, copy.attachments.count
2415 2435 end
2416 2436
2417 2437 def test_create_as_copy_with_attachments_should_add_new_files
2418 2438 @request.session[:user_id] = 2
2419 2439 issue = Issue.find(3)
2420 2440 count = issue.attachments.count
2421 2441 assert count > 0
2422 2442
2423 2443 assert_difference 'Issue.count' do
2424 2444 assert_difference 'Attachment.count', count + 1 do
2425 2445 assert_no_difference 'Journal.count' do
2426 2446 post :create, :project_id => 1, :copy_from => 3,
2427 2447 :issue => {:project_id => '1', :tracker_id => '3', :status_id => '1', :subject => 'Copy with attachments'},
2428 2448 :copy_attachments => '1',
2429 2449 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'test file'}}
2430 2450 end
2431 2451 end
2432 2452 end
2433 2453 copy = Issue.first(:order => 'id DESC')
2434 2454 assert_equal count + 1, copy.attachments.count
2435 2455 end
2436 2456
2437 2457 def test_create_as_copy_should_add_relation_with_copied_issue
2438 2458 @request.session[:user_id] = 2
2439 2459
2440 2460 assert_difference 'Issue.count' do
2441 2461 assert_difference 'IssueRelation.count' do
2442 2462 post :create, :project_id => 1, :copy_from => 1,
2443 2463 :issue => {:project_id => '1', :tracker_id => '3', :status_id => '1', :subject => 'Copy'}
2444 2464 end
2445 2465 end
2446 2466 copy = Issue.first(:order => 'id DESC')
2447 2467 assert_equal 1, copy.relations.size
2448 2468 end
2449 2469
2450 2470 def test_create_as_copy_should_copy_subtasks
2451 2471 @request.session[:user_id] = 2
2452 2472 issue = Issue.generate_with_descendants!
2453 2473 count = issue.descendants.count
2454 2474
2455 2475 assert_difference 'Issue.count', count+1 do
2456 2476 assert_no_difference 'Journal.count' do
2457 2477 post :create, :project_id => 1, :copy_from => issue.id,
2458 2478 :issue => {:project_id => '1', :tracker_id => '3', :status_id => '1', :subject => 'Copy with subtasks'},
2459 2479 :copy_subtasks => '1'
2460 2480 end
2461 2481 end
2462 2482 copy = Issue.where(:parent_id => nil).first(:order => 'id DESC')
2463 2483 assert_equal count, copy.descendants.count
2464 2484 assert_equal issue.descendants.map(&:subject).sort, copy.descendants.map(&:subject).sort
2465 2485 end
2466 2486
2467 2487 def test_create_as_copy_without_copy_subtasks_option_should_not_copy_subtasks
2468 2488 @request.session[:user_id] = 2
2469 2489 issue = Issue.generate_with_descendants!
2470 2490
2471 2491 assert_difference 'Issue.count', 1 do
2472 2492 assert_no_difference 'Journal.count' do
2473 2493 post :create, :project_id => 1, :copy_from => 3,
2474 2494 :issue => {:project_id => '1', :tracker_id => '3', :status_id => '1', :subject => 'Copy with subtasks'}
2475 2495 end
2476 2496 end
2477 2497 copy = Issue.where(:parent_id => nil).first(:order => 'id DESC')
2478 2498 assert_equal 0, copy.descendants.count
2479 2499 end
2480 2500
2481 2501 def test_create_as_copy_with_failure
2482 2502 @request.session[:user_id] = 2
2483 2503 post :create, :project_id => 1, :copy_from => 1,
2484 2504 :issue => {:project_id => '2', :tracker_id => '3', :status_id => '1', :subject => ''}
2485 2505
2486 2506 assert_response :success
2487 2507 assert_template 'new'
2488 2508
2489 2509 assert_not_nil assigns(:issue)
2490 2510 assert assigns(:issue).copy?
2491 2511
2492 2512 assert_tag 'form', :attributes => {:id => 'issue-form', :action => '/projects/ecookbook/issues'}
2493 2513 assert_tag 'select', :attributes => {:name => 'issue[project_id]'}
2494 2514 assert_tag 'select', :attributes => {:name => 'issue[project_id]'},
2495 2515 :child => {:tag => 'option', :attributes => {:value => '1', :selected => nil}, :content => 'eCookbook'}
2496 2516 assert_tag 'select', :attributes => {:name => 'issue[project_id]'},
2497 2517 :child => {:tag => 'option', :attributes => {:value => '2', :selected => 'selected'}, :content => 'OnlineStore'}
2498 2518 assert_tag 'input', :attributes => {:name => 'copy_from', :value => '1'}
2499 2519 end
2500 2520
2501 2521 def test_create_as_copy_on_project_without_permission_should_ignore_target_project
2502 2522 @request.session[:user_id] = 2
2503 2523 assert !User.find(2).member_of?(Project.find(4))
2504 2524
2505 2525 assert_difference 'Issue.count' do
2506 2526 post :create, :project_id => 1, :copy_from => 1,
2507 2527 :issue => {:project_id => '4', :tracker_id => '3', :status_id => '1', :subject => 'Copy'}
2508 2528 end
2509 2529 issue = Issue.first(:order => 'id DESC')
2510 2530 assert_equal 1, issue.project_id
2511 2531 end
2512 2532
2513 2533 def test_get_edit
2514 2534 @request.session[:user_id] = 2
2515 2535 get :edit, :id => 1
2516 2536 assert_response :success
2517 2537 assert_template 'edit'
2518 2538 assert_not_nil assigns(:issue)
2519 2539 assert_equal Issue.find(1), assigns(:issue)
2520 2540
2521 2541 # Be sure we don't display inactive IssuePriorities
2522 2542 assert ! IssuePriority.find(15).active?
2523 2543 assert_no_tag :option, :attributes => {:value => '15'},
2524 2544 :parent => {:tag => 'select', :attributes => {:id => 'issue_priority_id'} }
2525 2545 end
2526 2546
2527 2547 def test_get_edit_should_display_the_time_entry_form_with_log_time_permission
2528 2548 @request.session[:user_id] = 2
2529 2549 Role.find_by_name('Manager').update_attribute :permissions, [:view_issues, :edit_issues, :log_time]
2530 2550
2531 2551 get :edit, :id => 1
2532 2552 assert_tag 'input', :attributes => {:name => 'time_entry[hours]'}
2533 2553 end
2534 2554
2535 2555 def test_get_edit_should_not_display_the_time_entry_form_without_log_time_permission
2536 2556 @request.session[:user_id] = 2
2537 2557 Role.find_by_name('Manager').remove_permission! :log_time
2538 2558
2539 2559 get :edit, :id => 1
2540 2560 assert_no_tag 'input', :attributes => {:name => 'time_entry[hours]'}
2541 2561 end
2542 2562
2543 2563 def test_get_edit_with_params
2544 2564 @request.session[:user_id] = 2
2545 2565 get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 },
2546 2566 :time_entry => { :hours => '2.5', :comments => 'test_get_edit_with_params', :activity_id => TimeEntryActivity.first.id }
2547 2567 assert_response :success
2548 2568 assert_template 'edit'
2549 2569
2550 2570 issue = assigns(:issue)
2551 2571 assert_not_nil issue
2552 2572
2553 2573 assert_equal 5, issue.status_id
2554 2574 assert_tag :select, :attributes => { :name => 'issue[status_id]' },
2555 2575 :child => { :tag => 'option',
2556 2576 :content => 'Closed',
2557 2577 :attributes => { :selected => 'selected' } }
2558 2578
2559 2579 assert_equal 7, issue.priority_id
2560 2580 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
2561 2581 :child => { :tag => 'option',
2562 2582 :content => 'Urgent',
2563 2583 :attributes => { :selected => 'selected' } }
2564 2584
2565 2585 assert_tag :input, :attributes => { :name => 'time_entry[hours]', :value => '2.5' }
2566 2586 assert_tag :select, :attributes => { :name => 'time_entry[activity_id]' },
2567 2587 :child => { :tag => 'option',
2568 2588 :attributes => { :selected => 'selected', :value => TimeEntryActivity.first.id } }
2569 2589 assert_tag :input, :attributes => { :name => 'time_entry[comments]', :value => 'test_get_edit_with_params' }
2570 2590 end
2571 2591
2572 2592 def test_get_edit_with_multi_custom_field
2573 2593 field = CustomField.find(1)
2574 2594 field.update_attribute :multiple, true
2575 2595 issue = Issue.find(1)
2576 2596 issue.custom_field_values = {1 => ['MySQL', 'Oracle']}
2577 2597 issue.save!
2578 2598
2579 2599 @request.session[:user_id] = 2
2580 2600 get :edit, :id => 1
2581 2601 assert_response :success
2582 2602 assert_template 'edit'
2583 2603
2584 2604 assert_tag 'select', :attributes => {:name => 'issue[custom_field_values][1][]', :multiple => 'multiple'}
2585 2605 assert_tag 'select', :attributes => {:name => 'issue[custom_field_values][1][]'},
2586 2606 :child => {:tag => 'option', :attributes => {:value => 'MySQL', :selected => 'selected'}}
2587 2607 assert_tag 'select', :attributes => {:name => 'issue[custom_field_values][1][]'},
2588 2608 :child => {:tag => 'option', :attributes => {:value => 'PostgreSQL', :selected => nil}}
2589 2609 assert_tag 'select', :attributes => {:name => 'issue[custom_field_values][1][]'},
2590 2610 :child => {:tag => 'option', :attributes => {:value => 'Oracle', :selected => 'selected'}}
2591 2611 end
2592 2612
2593 2613 def test_update_edit_form
2594 2614 @request.session[:user_id] = 2
2595 2615 xhr :put, :new, :project_id => 1,
2596 2616 :id => 1,
2597 2617 :issue => {:tracker_id => 2,
2598 2618 :subject => 'This is the test_new issue',
2599 2619 :description => 'This is the description',
2600 2620 :priority_id => 5}
2601 2621 assert_response :success
2602 2622 assert_equal 'text/javascript', response.content_type
2603 2623 assert_template 'update_form'
2604 2624 assert_template 'form'
2605 2625
2606 2626 issue = assigns(:issue)
2607 2627 assert_kind_of Issue, issue
2608 2628 assert_equal 1, issue.id
2609 2629 assert_equal 1, issue.project_id
2610 2630 assert_equal 2, issue.tracker_id
2611 2631 assert_equal 'This is the test_new issue', issue.subject
2612 2632 end
2613 2633
2614 2634 def test_update_edit_form_should_keep_issue_author
2615 2635 @request.session[:user_id] = 3
2616 2636 xhr :put, :new, :project_id => 1, :id => 1, :issue => {:subject => 'Changed'}
2617 2637 assert_response :success
2618 2638 assert_equal 'text/javascript', response.content_type
2619 2639
2620 2640 issue = assigns(:issue)
2621 2641 assert_equal User.find(2), issue.author
2622 2642 assert_equal 2, issue.author_id
2623 2643 assert_not_equal User.current, issue.author
2624 2644 end
2625 2645
2626 2646 def test_update_edit_form_should_propose_transitions_based_on_initial_status
2627 2647 @request.session[:user_id] = 2
2628 2648 WorkflowTransition.delete_all
2629 2649 WorkflowTransition.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :new_status_id => 1)
2630 2650 WorkflowTransition.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :new_status_id => 5)
2631 2651 WorkflowTransition.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 5, :new_status_id => 4)
2632 2652
2633 2653 xhr :put, :new, :project_id => 1,
2634 2654 :id => 2,
2635 2655 :issue => {:tracker_id => 2,
2636 2656 :status_id => 5,
2637 2657 :subject => 'This is an issue'}
2638 2658
2639 2659 assert_equal 5, assigns(:issue).status_id
2640 2660 assert_equal [1,2,5], assigns(:allowed_statuses).map(&:id).sort
2641 2661 end
2642 2662
2643 2663 def test_update_edit_form_with_project_change
2644 2664 @request.session[:user_id] = 2
2645 2665 xhr :put, :new, :project_id => 1,
2646 2666 :id => 1,
2647 2667 :issue => {:project_id => 2,
2648 2668 :tracker_id => 2,
2649 2669 :subject => 'This is the test_new issue',
2650 2670 :description => 'This is the description',
2651 2671 :priority_id => 5}
2652 2672 assert_response :success
2653 2673 assert_template 'form'
2654 2674
2655 2675 issue = assigns(:issue)
2656 2676 assert_kind_of Issue, issue
2657 2677 assert_equal 1, issue.id
2658 2678 assert_equal 2, issue.project_id
2659 2679 assert_equal 2, issue.tracker_id
2660 2680 assert_equal 'This is the test_new issue', issue.subject
2661 2681 end
2662 2682
2663 2683 def test_put_update_without_custom_fields_param
2664 2684 @request.session[:user_id] = 2
2665 2685 ActionMailer::Base.deliveries.clear
2666 2686
2667 2687 issue = Issue.find(1)
2668 2688 assert_equal '125', issue.custom_value_for(2).value
2669 2689 old_subject = issue.subject
2670 2690 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
2671 2691
2672 2692 assert_difference('Journal.count') do
2673 2693 assert_difference('JournalDetail.count', 2) do
2674 2694 put :update, :id => 1, :issue => {:subject => new_subject,
2675 2695 :priority_id => '6',
2676 2696 :category_id => '1' # no change
2677 2697 }
2678 2698 end
2679 2699 end
2680 2700 assert_redirected_to :action => 'show', :id => '1'
2681 2701 issue.reload
2682 2702 assert_equal new_subject, issue.subject
2683 2703 # Make sure custom fields were not cleared
2684 2704 assert_equal '125', issue.custom_value_for(2).value
2685 2705
2686 2706 mail = ActionMailer::Base.deliveries.last
2687 2707 assert_not_nil mail
2688 2708 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
2689 2709 assert_mail_body_match "Subject changed from #{old_subject} to #{new_subject}", mail
2690 2710 end
2691 2711
2692 2712 def test_put_update_with_project_change
2693 2713 @request.session[:user_id] = 2
2694 2714 ActionMailer::Base.deliveries.clear
2695 2715
2696 2716 assert_difference('Journal.count') do
2697 2717 assert_difference('JournalDetail.count', 3) do
2698 2718 put :update, :id => 1, :issue => {:project_id => '2',
2699 2719 :tracker_id => '1', # no change
2700 2720 :priority_id => '6',
2701 2721 :category_id => '3'
2702 2722 }
2703 2723 end
2704 2724 end
2705 2725 assert_redirected_to :action => 'show', :id => '1'
2706 2726 issue = Issue.find(1)
2707 2727 assert_equal 2, issue.project_id
2708 2728 assert_equal 1, issue.tracker_id
2709 2729 assert_equal 6, issue.priority_id
2710 2730 assert_equal 3, issue.category_id
2711 2731
2712 2732 mail = ActionMailer::Base.deliveries.last
2713 2733 assert_not_nil mail
2714 2734 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
2715 2735 assert_mail_body_match "Project changed from eCookbook to OnlineStore", mail
2716 2736 end
2717 2737
2718 2738 def test_put_update_with_tracker_change
2719 2739 @request.session[:user_id] = 2
2720 2740 ActionMailer::Base.deliveries.clear
2721 2741
2722 2742 assert_difference('Journal.count') do
2723 2743 assert_difference('JournalDetail.count', 2) do
2724 2744 put :update, :id => 1, :issue => {:project_id => '1',
2725 2745 :tracker_id => '2',
2726 2746 :priority_id => '6'
2727 2747 }
2728 2748 end
2729 2749 end
2730 2750 assert_redirected_to :action => 'show', :id => '1'
2731 2751 issue = Issue.find(1)
2732 2752 assert_equal 1, issue.project_id
2733 2753 assert_equal 2, issue.tracker_id
2734 2754 assert_equal 6, issue.priority_id
2735 2755 assert_equal 1, issue.category_id
2736 2756
2737 2757 mail = ActionMailer::Base.deliveries.last
2738 2758 assert_not_nil mail
2739 2759 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
2740 2760 assert_mail_body_match "Tracker changed from Bug to Feature request", mail
2741 2761 end
2742 2762
2743 2763 def test_put_update_with_custom_field_change
2744 2764 @request.session[:user_id] = 2
2745 2765 issue = Issue.find(1)
2746 2766 assert_equal '125', issue.custom_value_for(2).value
2747 2767
2748 2768 assert_difference('Journal.count') do
2749 2769 assert_difference('JournalDetail.count', 3) do
2750 2770 put :update, :id => 1, :issue => {:subject => 'Custom field change',
2751 2771 :priority_id => '6',
2752 2772 :category_id => '1', # no change
2753 2773 :custom_field_values => { '2' => 'New custom value' }
2754 2774 }
2755 2775 end
2756 2776 end
2757 2777 assert_redirected_to :action => 'show', :id => '1'
2758 2778 issue.reload
2759 2779 assert_equal 'New custom value', issue.custom_value_for(2).value
2760 2780
2761 2781 mail = ActionMailer::Base.deliveries.last
2762 2782 assert_not_nil mail
2763 2783 assert_mail_body_match "Searchable field changed from 125 to New custom value", mail
2764 2784 end
2765 2785
2766 2786 def test_put_update_with_multi_custom_field_change
2767 2787 field = CustomField.find(1)
2768 2788 field.update_attribute :multiple, true
2769 2789 issue = Issue.find(1)
2770 2790 issue.custom_field_values = {1 => ['MySQL', 'Oracle']}
2771 2791 issue.save!
2772 2792
2773 2793 @request.session[:user_id] = 2
2774 2794 assert_difference('Journal.count') do
2775 2795 assert_difference('JournalDetail.count', 3) do
2776 2796 put :update, :id => 1,
2777 2797 :issue => {
2778 2798 :subject => 'Custom field change',
2779 2799 :custom_field_values => { '1' => ['', 'Oracle', 'PostgreSQL'] }
2780 2800 }
2781 2801 end
2782 2802 end
2783 2803 assert_redirected_to :action => 'show', :id => '1'
2784 2804 assert_equal ['Oracle', 'PostgreSQL'], Issue.find(1).custom_field_value(1).sort
2785 2805 end
2786 2806
2787 2807 def test_put_update_with_status_and_assignee_change
2788 2808 issue = Issue.find(1)
2789 2809 assert_equal 1, issue.status_id
2790 2810 @request.session[:user_id] = 2
2791 2811 assert_difference('TimeEntry.count', 0) do
2792 2812 put :update,
2793 2813 :id => 1,
2794 2814 :issue => { :status_id => 2, :assigned_to_id => 3, :notes => 'Assigned to dlopper' },
2795 2815 :time_entry => { :hours => '', :comments => '', :activity_id => TimeEntryActivity.first }
2796 2816 end
2797 2817 assert_redirected_to :action => 'show', :id => '1'
2798 2818 issue.reload
2799 2819 assert_equal 2, issue.status_id
2800 2820 j = Journal.find(:first, :order => 'id DESC')
2801 2821 assert_equal 'Assigned to dlopper', j.notes
2802 2822 assert_equal 2, j.details.size
2803 2823
2804 2824 mail = ActionMailer::Base.deliveries.last
2805 2825 assert_mail_body_match "Status changed from New to Assigned", mail
2806 2826 # subject should contain the new status
2807 2827 assert mail.subject.include?("(#{ IssueStatus.find(2).name })")
2808 2828 end
2809 2829
2810 2830 def test_put_update_with_note_only
2811 2831 notes = 'Note added by IssuesControllerTest#test_update_with_note_only'
2812 2832 # anonymous user
2813 2833 put :update,
2814 2834 :id => 1,
2815 2835 :issue => { :notes => notes }
2816 2836 assert_redirected_to :action => 'show', :id => '1'
2817 2837 j = Journal.find(:first, :order => 'id DESC')
2818 2838 assert_equal notes, j.notes
2819 2839 assert_equal 0, j.details.size
2820 2840 assert_equal User.anonymous, j.user
2821 2841
2822 2842 mail = ActionMailer::Base.deliveries.last
2823 2843 assert_mail_body_match notes, mail
2824 2844 end
2825 2845
2826 2846 def test_put_update_with_private_note_only
2827 2847 notes = 'Private note'
2828 2848 @request.session[:user_id] = 2
2829 2849
2830 2850 assert_difference 'Journal.count' do
2831 2851 put :update, :id => 1, :issue => {:notes => notes, :private_notes => '1'}
2832 2852 assert_redirected_to :action => 'show', :id => '1'
2833 2853 end
2834 2854
2835 2855 j = Journal.order('id DESC').first
2836 2856 assert_equal notes, j.notes
2837 2857 assert_equal true, j.private_notes
2838 2858 end
2839 2859
2840 2860 def test_put_update_with_private_note_and_changes
2841 2861 notes = 'Private note'
2842 2862 @request.session[:user_id] = 2
2843 2863
2844 2864 assert_difference 'Journal.count', 2 do
2845 2865 put :update, :id => 1, :issue => {:subject => 'New subject', :notes => notes, :private_notes => '1'}
2846 2866 assert_redirected_to :action => 'show', :id => '1'
2847 2867 end
2848 2868
2849 2869 j = Journal.order('id DESC').first
2850 2870 assert_equal notes, j.notes
2851 2871 assert_equal true, j.private_notes
2852 2872 assert_equal 0, j.details.count
2853 2873
2854 2874 j = Journal.order('id DESC').offset(1).first
2855 2875 assert_nil j.notes
2856 2876 assert_equal false, j.private_notes
2857 2877 assert_equal 1, j.details.count
2858 2878 end
2859 2879
2860 2880 def test_put_update_with_note_and_spent_time
2861 2881 @request.session[:user_id] = 2
2862 2882 spent_hours_before = Issue.find(1).spent_hours
2863 2883 assert_difference('TimeEntry.count') do
2864 2884 put :update,
2865 2885 :id => 1,
2866 2886 :issue => { :notes => '2.5 hours added' },
2867 2887 :time_entry => { :hours => '2.5', :comments => 'test_put_update_with_note_and_spent_time', :activity_id => TimeEntryActivity.first.id }
2868 2888 end
2869 2889 assert_redirected_to :action => 'show', :id => '1'
2870 2890
2871 2891 issue = Issue.find(1)
2872 2892
2873 2893 j = Journal.find(:first, :order => 'id DESC')
2874 2894 assert_equal '2.5 hours added', j.notes
2875 2895 assert_equal 0, j.details.size
2876 2896
2877 2897 t = issue.time_entries.find_by_comments('test_put_update_with_note_and_spent_time')
2878 2898 assert_not_nil t
2879 2899 assert_equal 2.5, t.hours
2880 2900 assert_equal spent_hours_before + 2.5, issue.spent_hours
2881 2901 end
2882 2902
2883 2903 def test_put_update_with_attachment_only
2884 2904 set_tmp_attachments_directory
2885 2905
2886 2906 # Delete all fixtured journals, a race condition can occur causing the wrong
2887 2907 # journal to get fetched in the next find.
2888 2908 Journal.delete_all
2889 2909
2890 2910 # anonymous user
2891 2911 assert_difference 'Attachment.count' do
2892 2912 put :update, :id => 1,
2893 2913 :issue => {:notes => ''},
2894 2914 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'test file'}}
2895 2915 end
2896 2916
2897 2917 assert_redirected_to :action => 'show', :id => '1'
2898 2918 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
2899 2919 assert j.notes.blank?
2900 2920 assert_equal 1, j.details.size
2901 2921 assert_equal 'testfile.txt', j.details.first.value
2902 2922 assert_equal User.anonymous, j.user
2903 2923
2904 2924 attachment = Attachment.first(:order => 'id DESC')
2905 2925 assert_equal Issue.find(1), attachment.container
2906 2926 assert_equal User.anonymous, attachment.author
2907 2927 assert_equal 'testfile.txt', attachment.filename
2908 2928 assert_equal 'text/plain', attachment.content_type
2909 2929 assert_equal 'test file', attachment.description
2910 2930 assert_equal 59, attachment.filesize
2911 2931 assert File.exists?(attachment.diskfile)
2912 2932 assert_equal 59, File.size(attachment.diskfile)
2913 2933
2914 2934 mail = ActionMailer::Base.deliveries.last
2915 2935 assert_mail_body_match 'testfile.txt', mail
2916 2936 end
2917 2937
2918 2938 def test_put_update_with_failure_should_save_attachments
2919 2939 set_tmp_attachments_directory
2920 2940 @request.session[:user_id] = 2
2921 2941
2922 2942 assert_no_difference 'Journal.count' do
2923 2943 assert_difference 'Attachment.count' do
2924 2944 put :update, :id => 1,
2925 2945 :issue => { :subject => '' },
2926 2946 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'test file'}}
2927 2947 assert_response :success
2928 2948 assert_template 'edit'
2929 2949 end
2930 2950 end
2931 2951
2932 2952 attachment = Attachment.first(:order => 'id DESC')
2933 2953 assert_equal 'testfile.txt', attachment.filename
2934 2954 assert File.exists?(attachment.diskfile)
2935 2955 assert_nil attachment.container
2936 2956
2937 2957 assert_tag 'input', :attributes => {:name => 'attachments[p0][token]', :value => attachment.token}
2938 2958 assert_tag 'span', :content => /testfile.txt/
2939 2959 end
2940 2960
2941 2961 def test_put_update_with_failure_should_keep_saved_attachments
2942 2962 set_tmp_attachments_directory
2943 2963 attachment = Attachment.create!(:file => uploaded_test_file("testfile.txt", "text/plain"), :author_id => 2)
2944 2964 @request.session[:user_id] = 2
2945 2965
2946 2966 assert_no_difference 'Journal.count' do
2947 2967 assert_no_difference 'Attachment.count' do
2948 2968 put :update, :id => 1,
2949 2969 :issue => { :subject => '' },
2950 2970 :attachments => {'p0' => {'token' => attachment.token}}
2951 2971 assert_response :success
2952 2972 assert_template 'edit'
2953 2973 end
2954 2974 end
2955 2975
2956 2976 assert_tag 'input', :attributes => {:name => 'attachments[p0][token]', :value => attachment.token}
2957 2977 assert_tag 'span', :content => /testfile.txt/
2958 2978 end
2959 2979
2960 2980 def test_put_update_should_attach_saved_attachments
2961 2981 set_tmp_attachments_directory
2962 2982 attachment = Attachment.create!(:file => uploaded_test_file("testfile.txt", "text/plain"), :author_id => 2)
2963 2983 @request.session[:user_id] = 2
2964 2984
2965 2985 assert_difference 'Journal.count' do
2966 2986 assert_difference 'JournalDetail.count' do
2967 2987 assert_no_difference 'Attachment.count' do
2968 2988 put :update, :id => 1,
2969 2989 :issue => {:notes => 'Attachment added'},
2970 2990 :attachments => {'p0' => {'token' => attachment.token}}
2971 2991 assert_redirected_to '/issues/1'
2972 2992 end
2973 2993 end
2974 2994 end
2975 2995
2976 2996 attachment.reload
2977 2997 assert_equal Issue.find(1), attachment.container
2978 2998
2979 2999 journal = Journal.first(:order => 'id DESC')
2980 3000 assert_equal 1, journal.details.size
2981 3001 assert_equal 'testfile.txt', journal.details.first.value
2982 3002 end
2983 3003
2984 3004 def test_put_update_with_attachment_that_fails_to_save
2985 3005 set_tmp_attachments_directory
2986 3006
2987 3007 # Delete all fixtured journals, a race condition can occur causing the wrong
2988 3008 # journal to get fetched in the next find.
2989 3009 Journal.delete_all
2990 3010
2991 3011 # Mock out the unsaved attachment
2992 3012 Attachment.any_instance.stubs(:create).returns(Attachment.new)
2993 3013
2994 3014 # anonymous user
2995 3015 put :update,
2996 3016 :id => 1,
2997 3017 :issue => {:notes => ''},
2998 3018 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
2999 3019 assert_redirected_to :action => 'show', :id => '1'
3000 3020 assert_equal '1 file(s) could not be saved.', flash[:warning]
3001 3021 end
3002 3022
3003 3023 def test_put_update_with_no_change
3004 3024 issue = Issue.find(1)
3005 3025 issue.journals.clear
3006 3026 ActionMailer::Base.deliveries.clear
3007 3027
3008 3028 put :update,
3009 3029 :id => 1,
3010 3030 :issue => {:notes => ''}
3011 3031 assert_redirected_to :action => 'show', :id => '1'
3012 3032
3013 3033 issue.reload
3014 3034 assert issue.journals.empty?
3015 3035 # No email should be sent
3016 3036 assert ActionMailer::Base.deliveries.empty?
3017 3037 end
3018 3038
3019 3039 def test_put_update_should_send_a_notification
3020 3040 @request.session[:user_id] = 2
3021 3041 ActionMailer::Base.deliveries.clear
3022 3042 issue = Issue.find(1)
3023 3043 old_subject = issue.subject
3024 3044 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
3025 3045
3026 3046 put :update, :id => 1, :issue => {:subject => new_subject,
3027 3047 :priority_id => '6',
3028 3048 :category_id => '1' # no change
3029 3049 }
3030 3050 assert_equal 1, ActionMailer::Base.deliveries.size
3031 3051 end
3032 3052
3033 3053 def test_put_update_with_invalid_spent_time_hours_only
3034 3054 @request.session[:user_id] = 2
3035 3055 notes = 'Note added by IssuesControllerTest#test_post_edit_with_invalid_spent_time'
3036 3056
3037 3057 assert_no_difference('Journal.count') do
3038 3058 put :update,
3039 3059 :id => 1,
3040 3060 :issue => {:notes => notes},
3041 3061 :time_entry => {"comments"=>"", "activity_id"=>"", "hours"=>"2z"}
3042 3062 end
3043 3063 assert_response :success
3044 3064 assert_template 'edit'
3045 3065
3046 3066 assert_error_tag :descendant => {:content => /Activity can&#x27;t be blank/}
3047 3067 assert_tag :textarea, :attributes => { :name => 'issue[notes]' }, :content => "\n"+notes
3048 3068 assert_tag :input, :attributes => { :name => 'time_entry[hours]', :value => "2z" }
3049 3069 end
3050 3070
3051 3071 def test_put_update_with_invalid_spent_time_comments_only
3052 3072 @request.session[:user_id] = 2
3053 3073 notes = 'Note added by IssuesControllerTest#test_post_edit_with_invalid_spent_time'
3054 3074
3055 3075 assert_no_difference('Journal.count') do
3056 3076 put :update,
3057 3077 :id => 1,
3058 3078 :issue => {:notes => notes},
3059 3079 :time_entry => {"comments"=>"this is my comment", "activity_id"=>"", "hours"=>""}
3060 3080 end
3061 3081 assert_response :success
3062 3082 assert_template 'edit'
3063 3083
3064 3084 assert_error_tag :descendant => {:content => /Activity can&#x27;t be blank/}
3065 3085 assert_error_tag :descendant => {:content => /Hours can&#x27;t be blank/}
3066 3086 assert_tag :textarea, :attributes => { :name => 'issue[notes]' }, :content => "\n"+notes
3067 3087 assert_tag :input, :attributes => { :name => 'time_entry[comments]', :value => "this is my comment" }
3068 3088 end
3069 3089
3070 3090 def test_put_update_should_allow_fixed_version_to_be_set_to_a_subproject
3071 3091 issue = Issue.find(2)
3072 3092 @request.session[:user_id] = 2
3073 3093
3074 3094 put :update,
3075 3095 :id => issue.id,
3076 3096 :issue => {
3077 3097 :fixed_version_id => 4
3078 3098 }
3079 3099
3080 3100 assert_response :redirect
3081 3101 issue.reload
3082 3102 assert_equal 4, issue.fixed_version_id
3083 3103 assert_not_equal issue.project_id, issue.fixed_version.project_id
3084 3104 end
3085 3105
3086 3106 def test_put_update_should_redirect_back_using_the_back_url_parameter
3087 3107 issue = Issue.find(2)
3088 3108 @request.session[:user_id] = 2
3089 3109
3090 3110 put :update,
3091 3111 :id => issue.id,
3092 3112 :issue => {
3093 3113 :fixed_version_id => 4
3094 3114 },
3095 3115 :back_url => '/issues'
3096 3116
3097 3117 assert_response :redirect
3098 3118 assert_redirected_to '/issues'
3099 3119 end
3100 3120
3101 3121 def test_put_update_should_not_redirect_back_using_the_back_url_parameter_off_the_host
3102 3122 issue = Issue.find(2)
3103 3123 @request.session[:user_id] = 2
3104 3124
3105 3125 put :update,
3106 3126 :id => issue.id,
3107 3127 :issue => {
3108 3128 :fixed_version_id => 4
3109 3129 },
3110 3130 :back_url => 'http://google.com'
3111 3131
3112 3132 assert_response :redirect
3113 3133 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue.id
3114 3134 end
3115 3135
3116 3136 def test_get_bulk_edit
3117 3137 @request.session[:user_id] = 2
3118 3138 get :bulk_edit, :ids => [1, 2]
3119 3139 assert_response :success
3120 3140 assert_template 'bulk_edit'
3121 3141
3122 3142 assert_tag :select, :attributes => {:name => 'issue[project_id]'}
3123 3143 assert_tag :input, :attributes => {:name => 'issue[parent_issue_id]'}
3124 3144
3125 3145 # Project specific custom field, date type
3126 3146 field = CustomField.find(9)
3127 3147 assert !field.is_for_all?
3128 3148 assert_equal 'date', field.field_format
3129 3149 assert_tag :input, :attributes => {:name => 'issue[custom_field_values][9]'}
3130 3150
3131 3151 # System wide custom field
3132 3152 assert CustomField.find(1).is_for_all?
3133 3153 assert_tag :select, :attributes => {:name => 'issue[custom_field_values][1]'}
3134 3154
3135 3155 # Be sure we don't display inactive IssuePriorities
3136 3156 assert ! IssuePriority.find(15).active?
3137 3157 assert_no_tag :option, :attributes => {:value => '15'},
3138 3158 :parent => {:tag => 'select', :attributes => {:id => 'issue_priority_id'} }
3139 3159 end
3140 3160
3141 3161 def test_get_bulk_edit_on_different_projects
3142 3162 @request.session[:user_id] = 2
3143 3163 get :bulk_edit, :ids => [1, 2, 6]
3144 3164 assert_response :success
3145 3165 assert_template 'bulk_edit'
3146 3166
3147 3167 # Can not set issues from different projects as children of an issue
3148 3168 assert_no_tag :input, :attributes => {:name => 'issue[parent_issue_id]'}
3149 3169
3150 3170 # Project specific custom field, date type
3151 3171 field = CustomField.find(9)
3152 3172 assert !field.is_for_all?
3153 3173 assert !field.project_ids.include?(Issue.find(6).project_id)
3154 3174 assert_no_tag :input, :attributes => {:name => 'issue[custom_field_values][9]'}
3155 3175 end
3156 3176
3157 3177 def test_get_bulk_edit_with_user_custom_field
3158 3178 field = IssueCustomField.create!(:name => 'Tester', :field_format => 'user', :is_for_all => true)
3159 3179
3160 3180 @request.session[:user_id] = 2
3161 3181 get :bulk_edit, :ids => [1, 2]
3162 3182 assert_response :success
3163 3183 assert_template 'bulk_edit'
3164 3184
3165 3185 assert_tag :select,
3166 3186 :attributes => {:name => "issue[custom_field_values][#{field.id}]", :class => 'user_cf'},
3167 3187 :children => {
3168 3188 :only => {:tag => 'option'},
3169 3189 :count => Project.find(1).users.count + 2 # "no change" + "none" options
3170 3190 }
3171 3191 end
3172 3192
3173 3193 def test_get_bulk_edit_with_version_custom_field
3174 3194 field = IssueCustomField.create!(:name => 'Affected version', :field_format => 'version', :is_for_all => true)
3175 3195
3176 3196 @request.session[:user_id] = 2
3177 3197 get :bulk_edit, :ids => [1, 2]
3178 3198 assert_response :success
3179 3199 assert_template 'bulk_edit'
3180 3200
3181 3201 assert_tag :select,
3182 3202 :attributes => {:name => "issue[custom_field_values][#{field.id}]"},
3183 3203 :children => {
3184 3204 :only => {:tag => 'option'},
3185 3205 :count => Project.find(1).shared_versions.count + 2 # "no change" + "none" options
3186 3206 }
3187 3207 end
3188 3208
3189 3209 def test_get_bulk_edit_with_multi_custom_field
3190 3210 field = CustomField.find(1)
3191 3211 field.update_attribute :multiple, true
3192 3212
3193 3213 @request.session[:user_id] = 2
3194 3214 get :bulk_edit, :ids => [1, 2]
3195 3215 assert_response :success
3196 3216 assert_template 'bulk_edit'
3197 3217
3198 3218 assert_tag :select,
3199 3219 :attributes => {:name => "issue[custom_field_values][1][]"},
3200 3220 :children => {
3201 3221 :only => {:tag => 'option'},
3202 3222 :count => field.possible_values.size + 1 # "none" options
3203 3223 }
3204 3224 end
3205 3225
3206 3226 def test_bulk_edit_should_only_propose_statuses_allowed_for_all_issues
3207 3227 WorkflowTransition.delete_all
3208 3228 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 1)
3209 3229 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 3)
3210 3230 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 4)
3211 3231 WorkflowTransition.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :new_status_id => 1)
3212 3232 WorkflowTransition.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :new_status_id => 3)
3213 3233 WorkflowTransition.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :new_status_id => 5)
3214 3234 @request.session[:user_id] = 2
3215 3235 get :bulk_edit, :ids => [1, 2]
3216 3236
3217 3237 assert_response :success
3218 3238 statuses = assigns(:available_statuses)
3219 3239 assert_not_nil statuses
3220 3240 assert_equal [1, 3], statuses.map(&:id).sort
3221 3241
3222 3242 assert_tag 'select', :attributes => {:name => 'issue[status_id]'},
3223 3243 :children => {:count => 3} # 2 statuses + "no change" option
3224 3244 end
3225 3245
3226 3246 def test_bulk_edit_should_propose_target_project_open_shared_versions
3227 3247 @request.session[:user_id] = 2
3228 3248 post :bulk_edit, :ids => [1, 2, 6], :issue => {:project_id => 1}
3229 3249 assert_response :success
3230 3250 assert_template 'bulk_edit'
3231 3251 assert_equal Project.find(1).shared_versions.open.all.sort, assigns(:versions).sort
3232 3252 assert_tag 'select',
3233 3253 :attributes => {:name => 'issue[fixed_version_id]'},
3234 3254 :descendant => {:tag => 'option', :content => '2.0'}
3235 3255 end
3236 3256
3237 3257 def test_bulk_edit_should_propose_target_project_categories
3238 3258 @request.session[:user_id] = 2
3239 3259 post :bulk_edit, :ids => [1, 2, 6], :issue => {:project_id => 1}
3240 3260 assert_response :success
3241 3261 assert_template 'bulk_edit'
3242 3262 assert_equal Project.find(1).issue_categories.sort, assigns(:categories).sort
3243 3263 assert_tag 'select',
3244 3264 :attributes => {:name => 'issue[category_id]'},
3245 3265 :descendant => {:tag => 'option', :content => 'Recipes'}
3246 3266 end
3247 3267
3248 3268 def test_bulk_update
3249 3269 @request.session[:user_id] = 2
3250 3270 # update issues priority
3251 3271 post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing',
3252 3272 :issue => {:priority_id => 7,
3253 3273 :assigned_to_id => '',
3254 3274 :custom_field_values => {'2' => ''}}
3255 3275
3256 3276 assert_response 302
3257 3277 # check that the issues were updated
3258 3278 assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id}
3259 3279
3260 3280 issue = Issue.find(1)
3261 3281 journal = issue.journals.find(:first, :order => 'created_on DESC')
3262 3282 assert_equal '125', issue.custom_value_for(2).value
3263 3283 assert_equal 'Bulk editing', journal.notes
3264 3284 assert_equal 1, journal.details.size
3265 3285 end
3266 3286
3267 3287 def test_bulk_update_with_group_assignee
3268 3288 group = Group.find(11)
3269 3289 project = Project.find(1)
3270 3290 project.members << Member.new(:principal => group, :roles => [Role.givable.first])
3271 3291
3272 3292 @request.session[:user_id] = 2
3273 3293 # update issues assignee
3274 3294 post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing',
3275 3295 :issue => {:priority_id => '',
3276 3296 :assigned_to_id => group.id,
3277 3297 :custom_field_values => {'2' => ''}}
3278 3298
3279 3299 assert_response 302
3280 3300 assert_equal [group, group], Issue.find_all_by_id([1, 2]).collect {|i| i.assigned_to}
3281 3301 end
3282 3302
3283 3303 def test_bulk_update_on_different_projects
3284 3304 @request.session[:user_id] = 2
3285 3305 # update issues priority
3286 3306 post :bulk_update, :ids => [1, 2, 6], :notes => 'Bulk editing',
3287 3307 :issue => {:priority_id => 7,
3288 3308 :assigned_to_id => '',
3289 3309 :custom_field_values => {'2' => ''}}
3290 3310
3291 3311 assert_response 302
3292 3312 # check that the issues were updated
3293 3313 assert_equal [7, 7, 7], Issue.find([1,2,6]).map(&:priority_id)
3294 3314
3295 3315 issue = Issue.find(1)
3296 3316 journal = issue.journals.find(:first, :order => 'created_on DESC')
3297 3317 assert_equal '125', issue.custom_value_for(2).value
3298 3318 assert_equal 'Bulk editing', journal.notes
3299 3319 assert_equal 1, journal.details.size
3300 3320 end
3301 3321
3302 3322 def test_bulk_update_on_different_projects_without_rights
3303 3323 @request.session[:user_id] = 3
3304 3324 user = User.find(3)
3305 3325 action = { :controller => "issues", :action => "bulk_update" }
3306 3326 assert user.allowed_to?(action, Issue.find(1).project)
3307 3327 assert ! user.allowed_to?(action, Issue.find(6).project)
3308 3328 post :bulk_update, :ids => [1, 6], :notes => 'Bulk should fail',
3309 3329 :issue => {:priority_id => 7,
3310 3330 :assigned_to_id => '',
3311 3331 :custom_field_values => {'2' => ''}}
3312 3332 assert_response 403
3313 3333 assert_not_equal "Bulk should fail", Journal.last.notes
3314 3334 end
3315 3335
3316 3336 def test_bullk_update_should_send_a_notification
3317 3337 @request.session[:user_id] = 2
3318 3338 ActionMailer::Base.deliveries.clear
3319 3339 post(:bulk_update,
3320 3340 {
3321 3341 :ids => [1, 2],
3322 3342 :notes => 'Bulk editing',
3323 3343 :issue => {
3324 3344 :priority_id => 7,
3325 3345 :assigned_to_id => '',
3326 3346 :custom_field_values => {'2' => ''}
3327 3347 }
3328 3348 })
3329 3349
3330 3350 assert_response 302
3331 3351 assert_equal 2, ActionMailer::Base.deliveries.size
3332 3352 end
3333 3353
3334 3354 def test_bulk_update_project
3335 3355 @request.session[:user_id] = 2
3336 3356 post :bulk_update, :ids => [1, 2], :issue => {:project_id => '2'}
3337 3357 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook'
3338 3358 # Issues moved to project 2
3339 3359 assert_equal 2, Issue.find(1).project_id
3340 3360 assert_equal 2, Issue.find(2).project_id
3341 3361 # No tracker change
3342 3362 assert_equal 1, Issue.find(1).tracker_id
3343 3363 assert_equal 2, Issue.find(2).tracker_id
3344 3364 end
3345 3365
3346 3366 def test_bulk_update_project_on_single_issue_should_follow_when_needed
3347 3367 @request.session[:user_id] = 2
3348 3368 post :bulk_update, :id => 1, :issue => {:project_id => '2'}, :follow => '1'
3349 3369 assert_redirected_to '/issues/1'
3350 3370 end
3351 3371
3352 3372 def test_bulk_update_project_on_multiple_issues_should_follow_when_needed
3353 3373 @request.session[:user_id] = 2
3354 3374 post :bulk_update, :id => [1, 2], :issue => {:project_id => '2'}, :follow => '1'
3355 3375 assert_redirected_to '/projects/onlinestore/issues'
3356 3376 end
3357 3377
3358 3378 def test_bulk_update_tracker
3359 3379 @request.session[:user_id] = 2
3360 3380 post :bulk_update, :ids => [1, 2], :issue => {:tracker_id => '2'}
3361 3381 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook'
3362 3382 assert_equal 2, Issue.find(1).tracker_id
3363 3383 assert_equal 2, Issue.find(2).tracker_id
3364 3384 end
3365 3385
3366 3386 def test_bulk_update_status
3367 3387 @request.session[:user_id] = 2
3368 3388 # update issues priority
3369 3389 post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing status',
3370 3390 :issue => {:priority_id => '',
3371 3391 :assigned_to_id => '',
3372 3392 :status_id => '5'}
3373 3393
3374 3394 assert_response 302
3375 3395 issue = Issue.find(1)
3376 3396 assert issue.closed?
3377 3397 end
3378 3398
3379 3399 def test_bulk_update_priority
3380 3400 @request.session[:user_id] = 2
3381 3401 post :bulk_update, :ids => [1, 2], :issue => {:priority_id => 6}
3382 3402
3383 3403 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook'
3384 3404 assert_equal 6, Issue.find(1).priority_id
3385 3405 assert_equal 6, Issue.find(2).priority_id
3386 3406 end
3387 3407
3388 3408 def test_bulk_update_with_notes
3389 3409 @request.session[:user_id] = 2
3390 3410 post :bulk_update, :ids => [1, 2], :notes => 'Moving two issues'
3391 3411
3392 3412 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook'
3393 3413 assert_equal 'Moving two issues', Issue.find(1).journals.sort_by(&:id).last.notes
3394 3414 assert_equal 'Moving two issues', Issue.find(2).journals.sort_by(&:id).last.notes
3395 3415 end
3396 3416
3397 3417 def test_bulk_update_parent_id
3398 3418 @request.session[:user_id] = 2
3399 3419 post :bulk_update, :ids => [1, 3],
3400 3420 :notes => 'Bulk editing parent',
3401 3421 :issue => {:priority_id => '', :assigned_to_id => '', :status_id => '', :parent_issue_id => '2'}
3402 3422
3403 3423 assert_response 302
3404 3424 parent = Issue.find(2)
3405 3425 assert_equal parent.id, Issue.find(1).parent_id
3406 3426 assert_equal parent.id, Issue.find(3).parent_id
3407 3427 assert_equal [1, 3], parent.children.collect(&:id).sort
3408 3428 end
3409 3429
3410 3430 def test_bulk_update_custom_field
3411 3431 @request.session[:user_id] = 2
3412 3432 # update issues priority
3413 3433 post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing custom field',
3414 3434 :issue => {:priority_id => '',
3415 3435 :assigned_to_id => '',
3416 3436 :custom_field_values => {'2' => '777'}}
3417 3437
3418 3438 assert_response 302
3419 3439
3420 3440 issue = Issue.find(1)
3421 3441 journal = issue.journals.find(:first, :order => 'created_on DESC')
3422 3442 assert_equal '777', issue.custom_value_for(2).value
3423 3443 assert_equal 1, journal.details.size
3424 3444 assert_equal '125', journal.details.first.old_value
3425 3445 assert_equal '777', journal.details.first.value
3426 3446 end
3427 3447
3428 3448 def test_bulk_update_custom_field_to_blank
3429 3449 @request.session[:user_id] = 2
3430 3450 post :bulk_update, :ids => [1, 3], :notes => 'Bulk editing custom field',
3431 3451 :issue => {:priority_id => '',
3432 3452 :assigned_to_id => '',
3433 3453 :custom_field_values => {'1' => '__none__'}}
3434 3454 assert_response 302
3435 3455 assert_equal '', Issue.find(1).custom_field_value(1)
3436 3456 assert_equal '', Issue.find(3).custom_field_value(1)
3437 3457 end
3438 3458
3439 3459 def test_bulk_update_multi_custom_field
3440 3460 field = CustomField.find(1)
3441 3461 field.update_attribute :multiple, true
3442 3462
3443 3463 @request.session[:user_id] = 2
3444 3464 post :bulk_update, :ids => [1, 2, 3], :notes => 'Bulk editing multi custom field',
3445 3465 :issue => {:priority_id => '',
3446 3466 :assigned_to_id => '',
3447 3467 :custom_field_values => {'1' => ['MySQL', 'Oracle']}}
3448 3468
3449 3469 assert_response 302
3450 3470
3451 3471 assert_equal ['MySQL', 'Oracle'], Issue.find(1).custom_field_value(1).sort
3452 3472 assert_equal ['MySQL', 'Oracle'], Issue.find(3).custom_field_value(1).sort
3453 3473 # the custom field is not associated with the issue tracker
3454 3474 assert_nil Issue.find(2).custom_field_value(1)
3455 3475 end
3456 3476
3457 3477 def test_bulk_update_multi_custom_field_to_blank
3458 3478 field = CustomField.find(1)
3459 3479 field.update_attribute :multiple, true
3460 3480
3461 3481 @request.session[:user_id] = 2
3462 3482 post :bulk_update, :ids => [1, 3], :notes => 'Bulk editing multi custom field',
3463 3483 :issue => {:priority_id => '',
3464 3484 :assigned_to_id => '',
3465 3485 :custom_field_values => {'1' => ['__none__']}}
3466 3486 assert_response 302
3467 3487 assert_equal [''], Issue.find(1).custom_field_value(1)
3468 3488 assert_equal [''], Issue.find(3).custom_field_value(1)
3469 3489 end
3470 3490
3471 3491 def test_bulk_update_unassign
3472 3492 assert_not_nil Issue.find(2).assigned_to
3473 3493 @request.session[:user_id] = 2
3474 3494 # unassign issues
3475 3495 post :bulk_update, :ids => [1, 2], :notes => 'Bulk unassigning', :issue => {:assigned_to_id => 'none'}
3476 3496 assert_response 302
3477 3497 # check that the issues were updated
3478 3498 assert_nil Issue.find(2).assigned_to
3479 3499 end
3480 3500
3481 3501 def test_post_bulk_update_should_allow_fixed_version_to_be_set_to_a_subproject
3482 3502 @request.session[:user_id] = 2
3483 3503
3484 3504 post :bulk_update, :ids => [1,2], :issue => {:fixed_version_id => 4}
3485 3505
3486 3506 assert_response :redirect
3487 3507 issues = Issue.find([1,2])
3488 3508 issues.each do |issue|
3489 3509 assert_equal 4, issue.fixed_version_id
3490 3510 assert_not_equal issue.project_id, issue.fixed_version.project_id
3491 3511 end
3492 3512 end
3493 3513
3494 3514 def test_post_bulk_update_should_redirect_back_using_the_back_url_parameter
3495 3515 @request.session[:user_id] = 2
3496 3516 post :bulk_update, :ids => [1,2], :back_url => '/issues'
3497 3517
3498 3518 assert_response :redirect
3499 3519 assert_redirected_to '/issues'
3500 3520 end
3501 3521
3502 3522 def test_post_bulk_update_should_not_redirect_back_using_the_back_url_parameter_off_the_host
3503 3523 @request.session[:user_id] = 2
3504 3524 post :bulk_update, :ids => [1,2], :back_url => 'http://google.com'
3505 3525
3506 3526 assert_response :redirect
3507 3527 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => Project.find(1).identifier
3508 3528 end
3509 3529
3510 3530 def test_bulk_update_with_failure_should_set_flash
3511 3531 @request.session[:user_id] = 2
3512 3532 Issue.update_all("subject = ''", "id = 2") # Make it invalid
3513 3533 post :bulk_update, :ids => [1, 2], :issue => {:priority_id => 6}
3514 3534
3515 3535 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook'
3516 3536 assert_equal 'Failed to save 1 issue(s) on 2 selected: #2.', flash[:error]
3517 3537 end
3518 3538
3519 3539 def test_get_bulk_copy
3520 3540 @request.session[:user_id] = 2
3521 3541 get :bulk_edit, :ids => [1, 2, 3], :copy => '1'
3522 3542 assert_response :success
3523 3543 assert_template 'bulk_edit'
3524 3544
3525 3545 issues = assigns(:issues)
3526 3546 assert_not_nil issues
3527 3547 assert_equal [1, 2, 3], issues.map(&:id).sort
3528 3548
3529 3549 assert_select 'input[name=copy_attachments]'
3530 3550 end
3531 3551
3532 3552 def test_bulk_copy_to_another_project
3533 3553 @request.session[:user_id] = 2
3534 3554 assert_difference 'Issue.count', 2 do
3535 3555 assert_no_difference 'Project.find(1).issues.count' do
3536 3556 post :bulk_update, :ids => [1, 2], :issue => {:project_id => '2'}, :copy => '1'
3537 3557 end
3538 3558 end
3539 3559 assert_redirected_to '/projects/ecookbook/issues'
3540 3560
3541 3561 copies = Issue.all(:order => 'id DESC', :limit => issues.size)
3542 3562 copies.each do |copy|
3543 3563 assert_equal 2, copy.project_id
3544 3564 end
3545 3565 end
3546 3566
3547 3567 def test_bulk_copy_should_allow_not_changing_the_issue_attributes
3548 3568 @request.session[:user_id] = 2
3549 3569 issues = [
3550 3570 Issue.create!(:project_id => 1, :tracker_id => 1, :status_id => 1, :priority_id => 2, :subject => 'issue 1', :author_id => 1, :assigned_to_id => nil),
3551 3571 Issue.create!(:project_id => 2, :tracker_id => 3, :status_id => 2, :priority_id => 1, :subject => 'issue 2', :author_id => 2, :assigned_to_id => 3)
3552 3572 ]
3553 3573
3554 3574 assert_difference 'Issue.count', issues.size do
3555 3575 post :bulk_update, :ids => issues.map(&:id), :copy => '1',
3556 3576 :issue => {
3557 3577 :project_id => '', :tracker_id => '', :assigned_to_id => '',
3558 3578 :status_id => '', :start_date => '', :due_date => ''
3559 3579 }
3560 3580 end
3561 3581
3562 3582 copies = Issue.all(:order => 'id DESC', :limit => issues.size)
3563 3583 issues.each do |orig|
3564 3584 copy = copies.detect {|c| c.subject == orig.subject}
3565 3585 assert_not_nil copy
3566 3586 assert_equal orig.project_id, copy.project_id
3567 3587 assert_equal orig.tracker_id, copy.tracker_id
3568 3588 assert_equal orig.status_id, copy.status_id
3569 3589 assert_equal orig.assigned_to_id, copy.assigned_to_id
3570 3590 assert_equal orig.priority_id, copy.priority_id
3571 3591 end
3572 3592 end
3573 3593
3574 3594 def test_bulk_copy_should_allow_changing_the_issue_attributes
3575 3595 # Fixes random test failure with Mysql
3576 3596 # where Issue.all(:limit => 2, :order => 'id desc', :conditions => {:project_id => 2})
3577 3597 # doesn't return the expected results
3578 3598 Issue.delete_all("project_id=2")
3579 3599
3580 3600 @request.session[:user_id] = 2
3581 3601 assert_difference 'Issue.count', 2 do
3582 3602 assert_no_difference 'Project.find(1).issues.count' do
3583 3603 post :bulk_update, :ids => [1, 2], :copy => '1',
3584 3604 :issue => {
3585 3605 :project_id => '2', :tracker_id => '', :assigned_to_id => '4',
3586 3606 :status_id => '1', :start_date => '2009-12-01', :due_date => '2009-12-31'
3587 3607 }
3588 3608 end
3589 3609 end
3590 3610
3591 3611 copied_issues = Issue.all(:limit => 2, :order => 'id desc', :conditions => {:project_id => 2})
3592 3612 assert_equal 2, copied_issues.size
3593 3613 copied_issues.each do |issue|
3594 3614 assert_equal 2, issue.project_id, "Project is incorrect"
3595 3615 assert_equal 4, issue.assigned_to_id, "Assigned to is incorrect"
3596 3616 assert_equal 1, issue.status_id, "Status is incorrect"
3597 3617 assert_equal '2009-12-01', issue.start_date.to_s, "Start date is incorrect"
3598 3618 assert_equal '2009-12-31', issue.due_date.to_s, "Due date is incorrect"
3599 3619 end
3600 3620 end
3601 3621
3602 3622 def test_bulk_copy_should_allow_adding_a_note
3603 3623 @request.session[:user_id] = 2
3604 3624 assert_difference 'Issue.count', 1 do
3605 3625 post :bulk_update, :ids => [1], :copy => '1',
3606 3626 :notes => 'Copying one issue',
3607 3627 :issue => {
3608 3628 :project_id => '', :tracker_id => '', :assigned_to_id => '4',
3609 3629 :status_id => '3', :start_date => '2009-12-01', :due_date => '2009-12-31'
3610 3630 }
3611 3631 end
3612 3632
3613 3633 issue = Issue.first(:order => 'id DESC')
3614 3634 assert_equal 1, issue.journals.size
3615 3635 journal = issue.journals.first
3616 3636 assert_equal 0, journal.details.size
3617 3637 assert_equal 'Copying one issue', journal.notes
3618 3638 end
3619 3639
3620 3640 def test_bulk_copy_should_allow_not_copying_the_attachments
3621 3641 attachment_count = Issue.find(3).attachments.size
3622 3642 assert attachment_count > 0
3623 3643 @request.session[:user_id] = 2
3624 3644
3625 3645 assert_difference 'Issue.count', 1 do
3626 3646 assert_no_difference 'Attachment.count' do
3627 3647 post :bulk_update, :ids => [3], :copy => '1',
3628 3648 :issue => {
3629 3649 :project_id => ''
3630 3650 }
3631 3651 end
3632 3652 end
3633 3653 end
3634 3654
3635 3655 def test_bulk_copy_should_allow_copying_the_attachments
3636 3656 attachment_count = Issue.find(3).attachments.size
3637 3657 assert attachment_count > 0
3638 3658 @request.session[:user_id] = 2
3639 3659
3640 3660 assert_difference 'Issue.count', 1 do
3641 3661 assert_difference 'Attachment.count', attachment_count do
3642 3662 post :bulk_update, :ids => [3], :copy => '1', :copy_attachments => '1',
3643 3663 :issue => {
3644 3664 :project_id => ''
3645 3665 }
3646 3666 end
3647 3667 end
3648 3668 end
3649 3669
3650 3670 def test_bulk_copy_should_add_relations_with_copied_issues
3651 3671 @request.session[:user_id] = 2
3652 3672
3653 3673 assert_difference 'Issue.count', 2 do
3654 3674 assert_difference 'IssueRelation.count', 2 do
3655 3675 post :bulk_update, :ids => [1, 3], :copy => '1',
3656 3676 :issue => {
3657 3677 :project_id => '1'
3658 3678 }
3659 3679 end
3660 3680 end
3661 3681 end
3662 3682
3663 3683 def test_bulk_copy_should_allow_not_copying_the_subtasks
3664 3684 issue = Issue.generate_with_descendants!
3665 3685 @request.session[:user_id] = 2
3666 3686
3667 3687 assert_difference 'Issue.count', 1 do
3668 3688 post :bulk_update, :ids => [issue.id], :copy => '1',
3669 3689 :issue => {
3670 3690 :project_id => ''
3671 3691 }
3672 3692 end
3673 3693 end
3674 3694
3675 3695 def test_bulk_copy_should_allow_copying_the_subtasks
3676 3696 issue = Issue.generate_with_descendants!
3677 3697 count = issue.descendants.count
3678 3698 @request.session[:user_id] = 2
3679 3699
3680 3700 assert_difference 'Issue.count', count+1 do
3681 3701 post :bulk_update, :ids => [issue.id], :copy => '1', :copy_subtasks => '1',
3682 3702 :issue => {
3683 3703 :project_id => ''
3684 3704 }
3685 3705 end
3686 3706 copy = Issue.where(:parent_id => nil).order("id DESC").first
3687 3707 assert_equal count, copy.descendants.count
3688 3708 end
3689 3709
3690 3710 def test_bulk_copy_should_not_copy_selected_subtasks_twice
3691 3711 issue = Issue.generate_with_descendants!
3692 3712 count = issue.descendants.count
3693 3713 @request.session[:user_id] = 2
3694 3714
3695 3715 assert_difference 'Issue.count', count+1 do
3696 3716 post :bulk_update, :ids => issue.self_and_descendants.map(&:id), :copy => '1', :copy_subtasks => '1',
3697 3717 :issue => {
3698 3718 :project_id => ''
3699 3719 }
3700 3720 end
3701 3721 copy = Issue.where(:parent_id => nil).order("id DESC").first
3702 3722 assert_equal count, copy.descendants.count
3703 3723 end
3704 3724
3705 3725 def test_bulk_copy_to_another_project_should_follow_when_needed
3706 3726 @request.session[:user_id] = 2
3707 3727 post :bulk_update, :ids => [1], :copy => '1', :issue => {:project_id => 2}, :follow => '1'
3708 3728 issue = Issue.first(:order => 'id DESC')
3709 3729 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
3710 3730 end
3711 3731
3712 3732 def test_destroy_issue_with_no_time_entries
3713 3733 assert_nil TimeEntry.find_by_issue_id(2)
3714 3734 @request.session[:user_id] = 2
3715 3735
3716 3736 assert_difference 'Issue.count', -1 do
3717 3737 delete :destroy, :id => 2
3718 3738 end
3719 3739 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
3720 3740 assert_nil Issue.find_by_id(2)
3721 3741 end
3722 3742
3723 3743 def test_destroy_issues_with_time_entries
3724 3744 @request.session[:user_id] = 2
3725 3745
3726 3746 assert_no_difference 'Issue.count' do
3727 3747 delete :destroy, :ids => [1, 3]
3728 3748 end
3729 3749 assert_response :success
3730 3750 assert_template 'destroy'
3731 3751 assert_not_nil assigns(:hours)
3732 3752 assert Issue.find_by_id(1) && Issue.find_by_id(3)
3733 3753 assert_tag 'form',
3734 3754 :descendant => {:tag => 'input', :attributes => {:name => '_method', :value => 'delete'}}
3735 3755 end
3736 3756
3737 3757 def test_destroy_issues_and_destroy_time_entries
3738 3758 @request.session[:user_id] = 2
3739 3759
3740 3760 assert_difference 'Issue.count', -2 do
3741 3761 assert_difference 'TimeEntry.count', -3 do
3742 3762 delete :destroy, :ids => [1, 3], :todo => 'destroy'
3743 3763 end
3744 3764 end
3745 3765 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
3746 3766 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
3747 3767 assert_nil TimeEntry.find_by_id([1, 2])
3748 3768 end
3749 3769
3750 3770 def test_destroy_issues_and_assign_time_entries_to_project
3751 3771 @request.session[:user_id] = 2
3752 3772
3753 3773 assert_difference 'Issue.count', -2 do
3754 3774 assert_no_difference 'TimeEntry.count' do
3755 3775 delete :destroy, :ids => [1, 3], :todo => 'nullify'
3756 3776 end
3757 3777 end
3758 3778 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
3759 3779 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
3760 3780 assert_nil TimeEntry.find(1).issue_id
3761 3781 assert_nil TimeEntry.find(2).issue_id
3762 3782 end
3763 3783
3764 3784 def test_destroy_issues_and_reassign_time_entries_to_another_issue
3765 3785 @request.session[:user_id] = 2
3766 3786
3767 3787 assert_difference 'Issue.count', -2 do
3768 3788 assert_no_difference 'TimeEntry.count' do
3769 3789 delete :destroy, :ids => [1, 3], :todo => 'reassign', :reassign_to_id => 2
3770 3790 end
3771 3791 end
3772 3792 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
3773 3793 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
3774 3794 assert_equal 2, TimeEntry.find(1).issue_id
3775 3795 assert_equal 2, TimeEntry.find(2).issue_id
3776 3796 end
3777 3797
3778 3798 def test_destroy_issues_from_different_projects
3779 3799 @request.session[:user_id] = 2
3780 3800
3781 3801 assert_difference 'Issue.count', -3 do
3782 3802 delete :destroy, :ids => [1, 2, 6], :todo => 'destroy'
3783 3803 end
3784 3804 assert_redirected_to :controller => 'issues', :action => 'index'
3785 3805 assert !(Issue.find_by_id(1) || Issue.find_by_id(2) || Issue.find_by_id(6))
3786 3806 end
3787 3807
3788 3808 def test_destroy_parent_and_child_issues
3789 3809 parent = Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :subject => 'Parent Issue')
3790 3810 child = Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :subject => 'Child Issue', :parent_issue_id => parent.id)
3791 3811 assert child.is_descendant_of?(parent.reload)
3792 3812
3793 3813 @request.session[:user_id] = 2
3794 3814 assert_difference 'Issue.count', -2 do
3795 3815 delete :destroy, :ids => [parent.id, child.id], :todo => 'destroy'
3796 3816 end
3797 3817 assert_response 302
3798 3818 end
3799 3819
3800 3820 def test_default_search_scope
3801 3821 get :index
3802 3822 assert_tag :div, :attributes => {:id => 'quick-search'},
3803 3823 :child => {:tag => 'form',
3804 3824 :child => {:tag => 'input', :attributes => {:name => 'issues', :type => 'hidden', :value => '1'}}}
3805 3825 end
3806 3826 end
General Comments 0
You need to be logged in to leave comments. Login now