##// END OF EJS Templates
Adds dynamic columns selection on the issue list (#4272)....
Jean-Philippe Lang -
r2991:66540afc0820
parent child
Show More
@@ -1,520 +1,521
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2008 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
20 20 default_search_scope :issues
21 21
22 22 before_filter :find_issue, :only => [:show, :edit, :reply]
23 23 before_filter :find_issues, :only => [:bulk_edit, :move, :destroy]
24 24 before_filter :find_project, :only => [:new, :update_form, :preview]
25 25 before_filter :authorize, :except => [:index, :changes, :gantt, :calendar, :preview, :update_form, :context_menu]
26 26 before_filter :find_optional_project, :only => [:index, :changes, :gantt, :calendar]
27 27 accept_key_auth :index, :show, :changes
28 28
29 29 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
30 30
31 31 helper :journals
32 32 helper :projects
33 33 include ProjectsHelper
34 34 helper :custom_fields
35 35 include CustomFieldsHelper
36 36 helper :issue_relations
37 37 include IssueRelationsHelper
38 38 helper :watchers
39 39 include WatchersHelper
40 40 helper :attachments
41 41 include AttachmentsHelper
42 42 helper :queries
43 43 helper :sort
44 44 include SortHelper
45 45 include IssuesHelper
46 46 helper :timelog
47 47 include Redmine::Export::PDF
48 48
49 49 verify :method => :post,
50 50 :only => :destroy,
51 51 :render => { :nothing => true, :status => :method_not_allowed }
52 52
53 53 def index
54 54 retrieve_query
55 55 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
56 56 sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.available_columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
57 57
58 58 if @query.valid?
59 59 limit = per_page_option
60 60 respond_to do |format|
61 61 format.html { }
62 62 format.atom { limit = Setting.feeds_limit.to_i }
63 63 format.csv { limit = Setting.issues_export_limit.to_i }
64 64 format.pdf { limit = Setting.issues_export_limit.to_i }
65 65 end
66 66
67 67 @issue_count = @query.issue_count
68 68 @issue_pages = Paginator.new self, @issue_count, limit, params['page']
69 69 @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
70 70 :order => sort_clause,
71 71 :offset => @issue_pages.current.offset,
72 72 :limit => limit)
73 73 @issue_count_by_group = @query.issue_count_by_group
74 74
75 75 respond_to do |format|
76 76 format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
77 77 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
78 78 format.csv { send_data(issues_to_csv(@issues, @project), :type => 'text/csv; header=present', :filename => 'export.csv') }
79 79 format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'export.pdf') }
80 80 end
81 81 else
82 82 # Send html if the query is not valid
83 83 render(:template => 'issues/index.rhtml', :layout => !request.xhr?)
84 84 end
85 85 rescue ActiveRecord::RecordNotFound
86 86 render_404
87 87 end
88 88
89 89 def changes
90 90 retrieve_query
91 91 sort_init 'id', 'desc'
92 92 sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.available_columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
93 93
94 94 if @query.valid?
95 95 @journals = @query.journals(:order => "#{Journal.table_name}.created_on DESC",
96 96 :limit => 25)
97 97 end
98 98 @title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
99 99 render :layout => false, :content_type => 'application/atom+xml'
100 100 rescue ActiveRecord::RecordNotFound
101 101 render_404
102 102 end
103 103
104 104 def show
105 105 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
106 106 @journals.each_with_index {|j,i| j.indice = i+1}
107 107 @journals.reverse! if User.current.wants_comments_in_reverse_order?
108 108 @changesets = @issue.changesets
109 109 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
110 110 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
111 111 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
112 112 @priorities = IssuePriority.all
113 113 @time_entry = TimeEntry.new
114 114 respond_to do |format|
115 115 format.html { render :template => 'issues/show.rhtml' }
116 116 format.atom { render :action => 'changes', :layout => false, :content_type => 'application/atom+xml' }
117 117 format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
118 118 end
119 119 end
120 120
121 121 # Add a new issue
122 122 # The new issue will be created from an existing one if copy_from parameter is given
123 123 def new
124 124 @issue = Issue.new
125 125 @issue.copy_from(params[:copy_from]) if params[:copy_from]
126 126 @issue.project = @project
127 127 # Tracker must be set before custom field values
128 128 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
129 129 if @issue.tracker.nil?
130 130 render_error l(:error_no_tracker_in_project)
131 131 return
132 132 end
133 133 if params[:issue].is_a?(Hash)
134 134 @issue.attributes = params[:issue]
135 135 @issue.watcher_user_ids = params[:issue]['watcher_user_ids'] if User.current.allowed_to?(:add_issue_watchers, @project)
136 136 end
137 137 @issue.author = User.current
138 138
139 139 default_status = IssueStatus.default
140 140 unless default_status
141 141 render_error l(:error_no_default_issue_status)
142 142 return
143 143 end
144 144 @issue.status = default_status
145 145 @allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(User.current.roles_for_project(@project), @issue.tracker)).uniq
146 146
147 147 if request.get? || request.xhr?
148 148 @issue.start_date ||= Date.today
149 149 else
150 150 requested_status = IssueStatus.find_by_id(params[:issue][:status_id])
151 151 # Check that the user is allowed to apply the requested status
152 152 @issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status
153 153 if @issue.save
154 154 attach_files(@issue, params[:attachments])
155 155 flash[:notice] = l(:notice_successful_create)
156 156 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
157 157 redirect_to(params[:continue] ? { :action => 'new', :tracker_id => @issue.tracker } :
158 158 { :action => 'show', :id => @issue })
159 159 return
160 160 end
161 161 end
162 162 @priorities = IssuePriority.all
163 163 render :layout => !request.xhr?
164 164 end
165 165
166 166 # Attributes that can be updated on workflow transition (without :edit permission)
167 167 # TODO: make it configurable (at least per role)
168 168 UPDATABLE_ATTRS_ON_TRANSITION = %w(status_id assigned_to_id fixed_version_id done_ratio) unless const_defined?(:UPDATABLE_ATTRS_ON_TRANSITION)
169 169
170 170 def edit
171 171 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
172 172 @priorities = IssuePriority.all
173 173 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
174 174 @time_entry = TimeEntry.new
175 175
176 176 @notes = params[:notes]
177 177 journal = @issue.init_journal(User.current, @notes)
178 178 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
179 179 if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue]
180 180 attrs = params[:issue].dup
181 181 attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed
182 182 attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s}
183 183 @issue.attributes = attrs
184 184 end
185 185
186 186 if request.post?
187 187 @time_entry = TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
188 188 @time_entry.attributes = params[:time_entry]
189 189 attachments = attach_files(@issue, params[:attachments])
190 190 attachments.each {|a| journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
191 191
192 192 call_hook(:controller_issues_edit_before_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
193 193
194 194 if (@time_entry.hours.nil? || @time_entry.valid?) && @issue.save
195 195 # Log spend time
196 196 if User.current.allowed_to?(:log_time, @project)
197 197 @time_entry.save
198 198 end
199 199 if !journal.new_record?
200 200 # Only send notification if something was actually changed
201 201 flash[:notice] = l(:notice_successful_update)
202 202 end
203 203 call_hook(:controller_issues_edit_after_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
204 204 redirect_to(params[:back_to] || {:action => 'show', :id => @issue})
205 205 end
206 206 end
207 207 rescue ActiveRecord::StaleObjectError
208 208 # Optimistic locking exception
209 209 flash.now[:error] = l(:notice_locking_conflict)
210 210 # Remove the previously added attachments if issue was not updated
211 211 attachments.each(&:destroy)
212 212 end
213 213
214 214 def reply
215 215 journal = Journal.find(params[:journal_id]) if params[:journal_id]
216 216 if journal
217 217 user = journal.user
218 218 text = journal.notes
219 219 else
220 220 user = @issue.author
221 221 text = @issue.description
222 222 end
223 223 content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
224 224 content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
225 225 render(:update) { |page|
226 226 page.<< "$('notes').value = \"#{content}\";"
227 227 page.show 'update'
228 228 page << "Form.Element.focus('notes');"
229 229 page << "Element.scrollTo('update');"
230 230 page << "$('notes').scrollTop = $('notes').scrollHeight - $('notes').clientHeight;"
231 231 }
232 232 end
233 233
234 234 # Bulk edit a set of issues
235 235 def bulk_edit
236 236 if request.post?
237 237 status = params[:status_id].blank? ? nil : IssueStatus.find_by_id(params[:status_id])
238 238 priority = params[:priority_id].blank? ? nil : IssuePriority.find_by_id(params[:priority_id])
239 239 assigned_to = (params[:assigned_to_id].blank? || params[:assigned_to_id] == 'none') ? nil : User.find_by_id(params[:assigned_to_id])
240 240 category = (params[:category_id].blank? || params[:category_id] == 'none') ? nil : @project.issue_categories.find_by_id(params[:category_id])
241 241 fixed_version = (params[:fixed_version_id].blank? || params[:fixed_version_id] == 'none') ? nil : @project.versions.find_by_id(params[:fixed_version_id])
242 242 custom_field_values = params[:custom_field_values] ? params[:custom_field_values].reject {|k,v| v.blank?} : nil
243 243
244 244 unsaved_issue_ids = []
245 245 @issues.each do |issue|
246 246 journal = issue.init_journal(User.current, params[:notes])
247 247 issue.priority = priority if priority
248 248 issue.assigned_to = assigned_to if assigned_to || params[:assigned_to_id] == 'none'
249 249 issue.category = category if category || params[:category_id] == 'none'
250 250 issue.fixed_version = fixed_version if fixed_version || params[:fixed_version_id] == 'none'
251 251 issue.start_date = params[:start_date] unless params[:start_date].blank?
252 252 issue.due_date = params[:due_date] unless params[:due_date].blank?
253 253 issue.done_ratio = params[:done_ratio] unless params[:done_ratio].blank?
254 254 issue.custom_field_values = custom_field_values if custom_field_values && !custom_field_values.empty?
255 255 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
256 256 # Don't save any change to the issue if the user is not authorized to apply the requested status
257 257 unless (status.nil? || (issue.new_statuses_allowed_to(User.current).include?(status) && issue.status = status)) && issue.save
258 258 # Keep unsaved issue ids to display them in flash error
259 259 unsaved_issue_ids << issue.id
260 260 end
261 261 end
262 262 if unsaved_issue_ids.empty?
263 263 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
264 264 else
265 265 flash[:error] = l(:notice_failed_to_save_issues, :count => unsaved_issue_ids.size,
266 266 :total => @issues.size,
267 267 :ids => '#' + unsaved_issue_ids.join(', #'))
268 268 end
269 269 redirect_to(params[:back_to] || {:controller => 'issues', :action => 'index', :project_id => @project})
270 270 return
271 271 end
272 272 # Find potential statuses the user could be allowed to switch issues to
273 273 @available_statuses = Workflow.find(:all, :include => :new_status,
274 274 :conditions => {:role_id => User.current.roles_for_project(@project).collect(&:id)}).collect(&:new_status).compact.uniq.sort
275 275 @custom_fields = @project.issue_custom_fields.select {|f| f.field_format == 'list'}
276 276 end
277 277
278 278 def move
279 279 @allowed_projects = []
280 280 # find projects to which the user is allowed to move the issue
281 281 if User.current.admin?
282 282 # admin is allowed to move issues to any active (visible) project
283 283 @allowed_projects = Project.find(:all, :conditions => Project.visible_by(User.current))
284 284 else
285 285 User.current.memberships.each {|m| @allowed_projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}}
286 286 end
287 287 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id]
288 288 @target_project ||= @project
289 289 @trackers = @target_project.trackers
290 290 if request.post?
291 291 new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id])
292 292 unsaved_issue_ids = []
293 293 moved_issues = []
294 294 @issues.each do |issue|
295 295 issue.init_journal(User.current)
296 296 if r = issue.move_to(@target_project, new_tracker, params[:copy_options])
297 297 moved_issues << r
298 298 else
299 299 unsaved_issue_ids << issue.id
300 300 end
301 301 end
302 302 if unsaved_issue_ids.empty?
303 303 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
304 304 else
305 305 flash[:error] = l(:notice_failed_to_save_issues, :count => unsaved_issue_ids.size,
306 306 :total => @issues.size,
307 307 :ids => '#' + unsaved_issue_ids.join(', #'))
308 308 end
309 309 if params[:follow]
310 310 if @issues.size == 1 && moved_issues.size == 1
311 311 redirect_to :controller => 'issues', :action => 'show', :id => moved_issues.first
312 312 else
313 313 redirect_to :controller => 'issues', :action => 'index', :project_id => (@target_project || @project)
314 314 end
315 315 else
316 316 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
317 317 end
318 318 return
319 319 end
320 320 render :layout => false if request.xhr?
321 321 end
322 322
323 323 def destroy
324 324 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
325 325 if @hours > 0
326 326 case params[:todo]
327 327 when 'destroy'
328 328 # nothing to do
329 329 when 'nullify'
330 330 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
331 331 when 'reassign'
332 332 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
333 333 if reassign_to.nil?
334 334 flash.now[:error] = l(:error_issue_not_found_in_project)
335 335 return
336 336 else
337 337 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
338 338 end
339 339 else
340 340 # display the destroy form
341 341 return
342 342 end
343 343 end
344 344 @issues.each(&:destroy)
345 345 redirect_to :action => 'index', :project_id => @project
346 346 end
347 347
348 348 def gantt
349 349 @gantt = Redmine::Helpers::Gantt.new(params)
350 350 retrieve_query
351 351 if @query.valid?
352 352 events = []
353 353 # Issues that have start and due dates
354 354 events += @query.issues(:include => [:tracker, :assigned_to, :priority],
355 355 :order => "start_date, due_date",
356 356 :conditions => ["(((start_date>=? and start_date<=?) or (due_date>=? and due_date<=?) or (start_date<? and due_date>?)) and start_date is not null and due_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to]
357 357 )
358 358 # Issues that don't have a due date but that are assigned to a version with a date
359 359 events += @query.issues(:include => [:tracker, :assigned_to, :priority, :fixed_version],
360 360 :order => "start_date, effective_date",
361 361 :conditions => ["(((start_date>=? and start_date<=?) or (effective_date>=? and effective_date<=?) or (start_date<? and effective_date>?)) and start_date is not null and due_date is null and effective_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to]
362 362 )
363 363 # Versions
364 364 events += @query.versions(:conditions => ["effective_date BETWEEN ? AND ?", @gantt.date_from, @gantt.date_to])
365 365
366 366 @gantt.events = events
367 367 end
368 368
369 369 basename = (@project ? "#{@project.identifier}-" : '') + 'gantt'
370 370
371 371 respond_to do |format|
372 372 format.html { render :template => "issues/gantt.rhtml", :layout => !request.xhr? }
373 373 format.png { send_data(@gantt.to_image, :disposition => 'inline', :type => 'image/png', :filename => "#{basename}.png") } if @gantt.respond_to?('to_image')
374 374 format.pdf { send_data(gantt_to_pdf(@gantt, @project), :type => 'application/pdf', :filename => "#{basename}.pdf") }
375 375 end
376 376 end
377 377
378 378 def calendar
379 379 if params[:year] and params[:year].to_i > 1900
380 380 @year = params[:year].to_i
381 381 if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
382 382 @month = params[:month].to_i
383 383 end
384 384 end
385 385 @year ||= Date.today.year
386 386 @month ||= Date.today.month
387 387
388 388 @calendar = Redmine::Helpers::Calendar.new(Date.civil(@year, @month, 1), current_language, :month)
389 389 retrieve_query
390 390 if @query.valid?
391 391 events = []
392 392 events += @query.issues(:include => [:tracker, :assigned_to, :priority],
393 393 :conditions => ["((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?))", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt]
394 394 )
395 395 events += @query.versions(:conditions => ["effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt])
396 396
397 397 @calendar.events = events
398 398 end
399 399
400 400 render :layout => false if request.xhr?
401 401 end
402 402
403 403 def context_menu
404 404 @issues = Issue.find_all_by_id(params[:ids], :include => :project)
405 405 if (@issues.size == 1)
406 406 @issue = @issues.first
407 407 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
408 408 end
409 409 projects = @issues.collect(&:project).compact.uniq
410 410 @project = projects.first if projects.size == 1
411 411
412 412 @can = {:edit => (@project && User.current.allowed_to?(:edit_issues, @project)),
413 413 :log_time => (@project && User.current.allowed_to?(:log_time, @project)),
414 414 :update => (@project && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && @allowed_statuses && !@allowed_statuses.empty?))),
415 415 :move => (@project && User.current.allowed_to?(:move_issues, @project)),
416 416 :copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
417 417 :delete => (@project && User.current.allowed_to?(:delete_issues, @project))
418 418 }
419 419 if @project
420 420 @assignables = @project.assignable_users
421 421 @assignables << @issue.assigned_to if @issue && @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
422 422 end
423 423
424 424 @priorities = IssuePriority.all.reverse
425 425 @statuses = IssueStatus.find(:all, :order => 'position')
426 426 @back = params[:back_url] || request.env['HTTP_REFERER']
427 427
428 428 render :layout => false
429 429 end
430 430
431 431 def update_form
432 432 @issue = Issue.new(params[:issue])
433 433 render :action => :new, :layout => false
434 434 end
435 435
436 436 def preview
437 437 @issue = @project.issues.find_by_id(params[:id]) unless params[:id].blank?
438 438 @attachements = @issue.attachments if @issue
439 439 @text = params[:notes] || (params[:issue] ? params[:issue][:description] : nil)
440 440 render :partial => 'common/preview'
441 441 end
442 442
443 443 private
444 444 def find_issue
445 445 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
446 446 @project = @issue.project
447 447 rescue ActiveRecord::RecordNotFound
448 448 render_404
449 449 end
450 450
451 451 # Filter for bulk operations
452 452 def find_issues
453 453 @issues = Issue.find_all_by_id(params[:id] || params[:ids])
454 454 raise ActiveRecord::RecordNotFound if @issues.empty?
455 455 projects = @issues.collect(&:project).compact.uniq
456 456 if projects.size == 1
457 457 @project = projects.first
458 458 else
459 459 # TODO: let users bulk edit/move/destroy issues from different projects
460 460 render_error 'Can not bulk edit/move/destroy issues from different projects' and return false
461 461 end
462 462 rescue ActiveRecord::RecordNotFound
463 463 render_404
464 464 end
465 465
466 466 def find_project
467 467 @project = Project.find(params[:project_id])
468 468 rescue ActiveRecord::RecordNotFound
469 469 render_404
470 470 end
471 471
472 472 def find_optional_project
473 473 @project = Project.find(params[:project_id]) unless params[:project_id].blank?
474 474 allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
475 475 allowed ? true : deny_access
476 476 rescue ActiveRecord::RecordNotFound
477 477 render_404
478 478 end
479 479
480 480 # Retrieve query from session or build a new query
481 481 def retrieve_query
482 482 if !params[:query_id].blank?
483 483 cond = "project_id IS NULL"
484 484 cond << " OR project_id = #{@project.id}" if @project
485 485 @query = Query.find(params[:query_id], :conditions => cond)
486 486 @query.project = @project
487 487 session[:query] = {:id => @query.id, :project_id => @query.project_id}
488 488 sort_clear
489 489 else
490 490 if params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
491 491 # Give it a name, required to be valid
492 492 @query = Query.new(:name => "_")
493 493 @query.project = @project
494 494 if params[:fields] and params[:fields].is_a? Array
495 495 params[:fields].each do |field|
496 496 @query.add_filter(field, params[:operators][field], params[:values][field])
497 497 end
498 498 else
499 499 @query.available_filters.keys.each do |field|
500 500 @query.add_short_filter(field, params[field]) if params[field]
501 501 end
502 502 end
503 503 @query.group_by = params[:group_by]
504 session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by}
504 @query.column_names = params[:query] && params[:query][:column_names]
505 session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names}
505 506 else
506 507 @query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
507 @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters], :group_by => session[:query][:group_by])
508 @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
508 509 @query.project = @project
509 510 end
510 511 end
511 512 end
512 513
513 514 # Rescues an invalid query statement. Just in case...
514 515 def query_statement_invalid(exception)
515 516 logger.error "Query::StatementInvalid: #{exception.message}" if logger
516 517 session.delete(:query)
517 518 sort_clear
518 519 render_error "An error occurred while executing the query and has been logged. Please report this error to your Redmine administrator."
519 520 end
520 521 end
@@ -1,539 +1,545
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2008 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 end
31 31
32 32 def caption
33 33 l("field_#{name}")
34 34 end
35 35
36 36 # Returns true if the column is sortable, otherwise false
37 37 def sortable?
38 38 !sortable.nil?
39 39 end
40 40 end
41 41
42 42 class QueryCustomFieldColumn < QueryColumn
43 43
44 44 def initialize(custom_field)
45 45 self.name = "cf_#{custom_field.id}".to_sym
46 46 self.sortable = custom_field.order_statement || false
47 47 if %w(list date bool int).include?(custom_field.field_format)
48 48 self.groupable = custom_field.order_statement
49 49 end
50 50 self.groupable ||= false
51 51 @cf = custom_field
52 52 end
53 53
54 54 def caption
55 55 @cf.name
56 56 end
57 57
58 58 def custom_field
59 59 @cf
60 60 end
61 61 end
62 62
63 63 class Query < ActiveRecord::Base
64 64 class StatementInvalid < ::ActiveRecord::StatementInvalid
65 65 end
66 66
67 67 belongs_to :project
68 68 belongs_to :user
69 69 serialize :filters
70 70 serialize :column_names
71 71 serialize :sort_criteria, Array
72 72
73 73 attr_protected :project_id, :user_id
74 74
75 75 validates_presence_of :name, :on => :save
76 76 validates_length_of :name, :maximum => 255
77 77
78 78 @@operators = { "=" => :label_equals,
79 79 "!" => :label_not_equals,
80 80 "o" => :label_open_issues,
81 81 "c" => :label_closed_issues,
82 82 "!*" => :label_none,
83 83 "*" => :label_all,
84 84 ">=" => :label_greater_or_equal,
85 85 "<=" => :label_less_or_equal,
86 86 "<t+" => :label_in_less_than,
87 87 ">t+" => :label_in_more_than,
88 88 "t+" => :label_in,
89 89 "t" => :label_today,
90 90 "w" => :label_this_week,
91 91 ">t-" => :label_less_than_ago,
92 92 "<t-" => :label_more_than_ago,
93 93 "t-" => :label_ago,
94 94 "~" => :label_contains,
95 95 "!~" => :label_not_contains }
96 96
97 97 cattr_reader :operators
98 98
99 99 @@operators_by_filter_type = { :list => [ "=", "!" ],
100 100 :list_status => [ "o", "=", "!", "c", "*" ],
101 101 :list_optional => [ "=", "!", "!*", "*" ],
102 102 :list_subprojects => [ "*", "!*", "=" ],
103 103 :date => [ "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-" ],
104 104 :date_past => [ ">t-", "<t-", "t-", "t", "w" ],
105 105 :string => [ "=", "~", "!", "!~" ],
106 106 :text => [ "~", "!~" ],
107 107 :integer => [ "=", ">=", "<=", "!*", "*" ] }
108 108
109 109 cattr_reader :operators_by_filter_type
110 110
111 111 @@available_columns = [
112 112 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
113 113 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
114 114 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
115 115 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
116 116 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
117 117 QueryColumn.new(:author),
118 118 QueryColumn.new(:assigned_to, :sortable => ["#{User.table_name}.lastname", "#{User.table_name}.firstname", "#{User.table_name}.id"], :groupable => true),
119 119 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
120 120 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
121 121 QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc', :groupable => true),
122 122 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
123 123 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
124 124 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
125 125 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
126 126 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
127 127 ]
128 128 cattr_reader :available_columns
129 129
130 130 def initialize(attributes = nil)
131 131 super attributes
132 132 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
133 133 end
134 134
135 135 def after_initialize
136 136 # Store the fact that project is nil (used in #editable_by?)
137 137 @is_for_all = project.nil?
138 138 end
139 139
140 140 def validate
141 141 filters.each_key do |field|
142 142 errors.add label_for(field), :blank unless
143 143 # filter requires one or more values
144 144 (values_for(field) and !values_for(field).first.blank?) or
145 145 # filter doesn't require any value
146 146 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
147 147 end if filters
148 148 end
149 149
150 150 def editable_by?(user)
151 151 return false unless user
152 152 # Admin can edit them all and regular users can edit their private queries
153 153 return true if user.admin? || (!is_public && self.user_id == user.id)
154 154 # Members can not edit public queries that are for all project (only admin is allowed to)
155 155 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
156 156 end
157 157
158 158 def available_filters
159 159 return @available_filters if @available_filters
160 160
161 161 trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
162 162
163 163 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
164 164 "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
165 165 "priority_id" => { :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } },
166 166 "subject" => { :type => :text, :order => 8 },
167 167 "created_on" => { :type => :date_past, :order => 9 },
168 168 "updated_on" => { :type => :date_past, :order => 10 },
169 169 "start_date" => { :type => :date, :order => 11 },
170 170 "due_date" => { :type => :date, :order => 12 },
171 171 "estimated_hours" => { :type => :integer, :order => 13 },
172 172 "done_ratio" => { :type => :integer, :order => 14 }}
173 173
174 174 user_values = []
175 175 user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
176 176 if project
177 177 user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] }
178 178 else
179 179 # members of the user's projects
180 180 user_values += User.current.projects.collect(&:users).flatten.uniq.sort.collect{|s| [s.name, s.id.to_s] }
181 181 end
182 182 @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values } unless user_values.empty?
183 183 @available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values } unless user_values.empty?
184 184
185 185 if User.current.logged?
186 186 @available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] }
187 187 end
188 188
189 189 if project
190 190 # project specific filters
191 191 unless @project.issue_categories.empty?
192 192 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => @project.issue_categories.collect{|s| [s.name, s.id.to_s] } }
193 193 end
194 194 unless @project.versions.empty?
195 195 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => @project.versions.sort.collect{|s| [s.name, s.id.to_s] } }
196 196 end
197 197 unless @project.descendants.active.empty?
198 198 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.descendants.visible.collect{|s| [s.name, s.id.to_s] } }
199 199 end
200 200 add_custom_fields_filters(@project.all_issue_custom_fields)
201 201 else
202 202 # global filters for cross project issue list
203 203 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
204 204 end
205 205 @available_filters
206 206 end
207 207
208 208 def add_filter(field, operator, values)
209 209 # values must be an array
210 210 return unless values and values.is_a? Array # and !values.first.empty?
211 211 # check if field is defined as an available filter
212 212 if available_filters.has_key? field
213 213 filter_options = available_filters[field]
214 214 # check if operator is allowed for that filter
215 215 #if @@operators_by_filter_type[filter_options[:type]].include? operator
216 216 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
217 217 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
218 218 #end
219 219 filters[field] = {:operator => operator, :values => values }
220 220 end
221 221 end
222 222
223 223 def add_short_filter(field, expression)
224 224 return unless expression
225 225 parms = expression.scan(/^(o|c|\!|\*)?(.*)$/).first
226 226 add_filter field, (parms[0] || "="), [parms[1] || ""]
227 227 end
228 228
229 229 def has_filter?(field)
230 230 filters and filters[field]
231 231 end
232 232
233 233 def operator_for(field)
234 234 has_filter?(field) ? filters[field][:operator] : nil
235 235 end
236 236
237 237 def values_for(field)
238 238 has_filter?(field) ? filters[field][:values] : nil
239 239 end
240 240
241 241 def label_for(field)
242 242 label = available_filters[field][:name] if available_filters.has_key?(field)
243 243 label ||= field.gsub(/\_id$/, "")
244 244 end
245 245
246 246 def available_columns
247 247 return @available_columns if @available_columns
248 248 @available_columns = Query.available_columns
249 249 @available_columns += (project ?
250 250 project.all_issue_custom_fields :
251 251 IssueCustomField.find(:all)
252 252 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
253 253 end
254 254
255 255 # Returns an array of columns that can be used to group the results
256 256 def groupable_columns
257 257 available_columns.select {|c| c.groupable}
258 258 end
259 259
260 260 def columns
261 261 if has_default_columns?
262 262 available_columns.select do |c|
263 263 # Adds the project column by default for cross-project lists
264 264 Setting.issue_list_default_columns.include?(c.name.to_s) || (c.name == :project && project.nil?)
265 265 end
266 266 else
267 267 # preserve the column_names order
268 268 column_names.collect {|name| available_columns.find {|col| col.name == name}}.compact
269 269 end
270 270 end
271 271
272 272 def column_names=(names)
273 names = names.select {|n| n.is_a?(Symbol) || !n.blank? } if names
274 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym } if names
273 if names
274 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
275 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
276 # Set column_names to nil if default columns
277 if names.map(&:to_s) == Setting.issue_list_default_columns
278 names = nil
279 end
280 end
275 281 write_attribute(:column_names, names)
276 282 end
277 283
278 284 def has_column?(column)
279 285 column_names && column_names.include?(column.name)
280 286 end
281 287
282 288 def has_default_columns?
283 289 column_names.nil? || column_names.empty?
284 290 end
285 291
286 292 def sort_criteria=(arg)
287 293 c = []
288 294 if arg.is_a?(Hash)
289 295 arg = arg.keys.sort.collect {|k| arg[k]}
290 296 end
291 297 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']}
292 298 write_attribute(:sort_criteria, c)
293 299 end
294 300
295 301 def sort_criteria
296 302 read_attribute(:sort_criteria) || []
297 303 end
298 304
299 305 def sort_criteria_key(arg)
300 306 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
301 307 end
302 308
303 309 def sort_criteria_order(arg)
304 310 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
305 311 end
306 312
307 313 # Returns the SQL sort order that should be prepended for grouping
308 314 def group_by_sort_order
309 315 if grouped? && (column = group_by_column)
310 316 column.sortable.is_a?(Array) ?
311 317 column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') :
312 318 "#{column.sortable} #{column.default_order}"
313 319 end
314 320 end
315 321
316 322 # Returns true if the query is a grouped query
317 323 def grouped?
318 324 !group_by.blank?
319 325 end
320 326
321 327 def group_by_column
322 328 groupable_columns.detect {|c| c.name.to_s == group_by}
323 329 end
324 330
325 331 def group_by_statement
326 332 group_by_column.groupable
327 333 end
328 334
329 335 def project_statement
330 336 project_clauses = []
331 337 if project && !@project.descendants.active.empty?
332 338 ids = [project.id]
333 339 if has_filter?("subproject_id")
334 340 case operator_for("subproject_id")
335 341 when '='
336 342 # include the selected subprojects
337 343 ids += values_for("subproject_id").each(&:to_i)
338 344 when '!*'
339 345 # main project only
340 346 else
341 347 # all subprojects
342 348 ids += project.descendants.collect(&:id)
343 349 end
344 350 elsif Setting.display_subprojects_issues?
345 351 ids += project.descendants.collect(&:id)
346 352 end
347 353 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
348 354 elsif project
349 355 project_clauses << "#{Project.table_name}.id = %d" % project.id
350 356 end
351 357 project_clauses << Project.allowed_to_condition(User.current, :view_issues)
352 358 project_clauses.join(' AND ')
353 359 end
354 360
355 361 def statement
356 362 # filters clauses
357 363 filters_clauses = []
358 364 filters.each_key do |field|
359 365 next if field == "subproject_id"
360 366 v = values_for(field).clone
361 367 next unless v and !v.empty?
362 368 operator = operator_for(field)
363 369
364 370 # "me" value subsitution
365 371 if %w(assigned_to_id author_id watcher_id).include?(field)
366 372 v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me")
367 373 end
368 374
369 375 sql = ''
370 376 if field =~ /^cf_(\d+)$/
371 377 # custom field
372 378 db_table = CustomValue.table_name
373 379 db_field = 'value'
374 380 is_custom_filter = true
375 381 sql << "#{Issue.table_name}.id IN (SELECT #{Issue.table_name}.id FROM #{Issue.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='Issue' AND #{db_table}.customized_id=#{Issue.table_name}.id AND #{db_table}.custom_field_id=#{$1} WHERE "
376 382 sql << sql_for_field(field, operator, v, db_table, db_field, true) + ')'
377 383 elsif field == 'watcher_id'
378 384 db_table = Watcher.table_name
379 385 db_field = 'user_id'
380 386 sql << "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND "
381 387 sql << sql_for_field(field, '=', v, db_table, db_field) + ')'
382 388 else
383 389 # regular field
384 390 db_table = Issue.table_name
385 391 db_field = field
386 392 sql << '(' + sql_for_field(field, operator, v, db_table, db_field) + ')'
387 393 end
388 394 filters_clauses << sql
389 395
390 396 end if filters and valid?
391 397
392 398 (filters_clauses << project_statement).join(' AND ')
393 399 end
394 400
395 401 # Returns the issue count
396 402 def issue_count
397 403 Issue.count(:include => [:status, :project], :conditions => statement)
398 404 rescue ::ActiveRecord::StatementInvalid => e
399 405 raise StatementInvalid.new(e.message)
400 406 end
401 407
402 408 # Returns the issue count by group or nil if query is not grouped
403 409 def issue_count_by_group
404 410 if grouped?
405 411 begin
406 412 # Rails will raise an (unexpected) RecordNotFound if there's only a nil group value
407 413 Issue.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement)
408 414 rescue ActiveRecord::RecordNotFound
409 415 {nil => issue_count}
410 416 end
411 417 else
412 418 nil
413 419 end
414 420 rescue ::ActiveRecord::StatementInvalid => e
415 421 raise StatementInvalid.new(e.message)
416 422 end
417 423
418 424 # Returns the issues
419 425 # Valid options are :order, :offset, :limit, :include, :conditions
420 426 def issues(options={})
421 427 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
422 428 order_option = nil if order_option.blank?
423 429
424 430 Issue.find :all, :include => ([:status, :project] + (options[:include] || [])).uniq,
425 431 :conditions => Query.merge_conditions(statement, options[:conditions]),
426 432 :order => order_option,
427 433 :limit => options[:limit],
428 434 :offset => options[:offset]
429 435 rescue ::ActiveRecord::StatementInvalid => e
430 436 raise StatementInvalid.new(e.message)
431 437 end
432 438
433 439 # Returns the journals
434 440 # Valid options are :order, :offset, :limit
435 441 def journals(options={})
436 442 Journal.find :all, :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
437 443 :conditions => statement,
438 444 :order => options[:order],
439 445 :limit => options[:limit],
440 446 :offset => options[:offset]
441 447 rescue ::ActiveRecord::StatementInvalid => e
442 448 raise StatementInvalid.new(e.message)
443 449 end
444 450
445 451 # Returns the versions
446 452 # Valid options are :conditions
447 453 def versions(options={})
448 454 Version.find :all, :include => :project,
449 455 :conditions => Query.merge_conditions(project_statement, options[:conditions])
450 456 rescue ::ActiveRecord::StatementInvalid => e
451 457 raise StatementInvalid.new(e.message)
452 458 end
453 459
454 460 private
455 461
456 462 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
457 463 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
458 464 sql = ''
459 465 case operator
460 466 when "="
461 467 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
462 468 when "!"
463 469 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
464 470 when "!*"
465 471 sql = "#{db_table}.#{db_field} IS NULL"
466 472 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
467 473 when "*"
468 474 sql = "#{db_table}.#{db_field} IS NOT NULL"
469 475 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
470 476 when ">="
471 477 sql = "#{db_table}.#{db_field} >= #{value.first.to_i}"
472 478 when "<="
473 479 sql = "#{db_table}.#{db_field} <= #{value.first.to_i}"
474 480 when "o"
475 481 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
476 482 when "c"
477 483 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
478 484 when ">t-"
479 485 sql = date_range_clause(db_table, db_field, - value.first.to_i, 0)
480 486 when "<t-"
481 487 sql = date_range_clause(db_table, db_field, nil, - value.first.to_i)
482 488 when "t-"
483 489 sql = date_range_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
484 490 when ">t+"
485 491 sql = date_range_clause(db_table, db_field, value.first.to_i, nil)
486 492 when "<t+"
487 493 sql = date_range_clause(db_table, db_field, 0, value.first.to_i)
488 494 when "t+"
489 495 sql = date_range_clause(db_table, db_field, value.first.to_i, value.first.to_i)
490 496 when "t"
491 497 sql = date_range_clause(db_table, db_field, 0, 0)
492 498 when "w"
493 499 from = l(:general_first_day_of_week) == '7' ?
494 500 # week starts on sunday
495 501 ((Date.today.cwday == 7) ? Time.now.at_beginning_of_day : Time.now.at_beginning_of_week - 1.day) :
496 502 # week starts on monday (Rails default)
497 503 Time.now.at_beginning_of_week
498 504 sql = "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(from), connection.quoted_date(from + 7.days)]
499 505 when "~"
500 506 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
501 507 when "!~"
502 508 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
503 509 end
504 510
505 511 return sql
506 512 end
507 513
508 514 def add_custom_fields_filters(custom_fields)
509 515 @available_filters ||= {}
510 516
511 517 custom_fields.select(&:is_filter?).each do |field|
512 518 case field.field_format
513 519 when "text"
514 520 options = { :type => :text, :order => 20 }
515 521 when "list"
516 522 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
517 523 when "date"
518 524 options = { :type => :date, :order => 20 }
519 525 when "bool"
520 526 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
521 527 else
522 528 options = { :type => :string, :order => 20 }
523 529 end
524 530 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
525 531 end
526 532 end
527 533
528 534 # Returns a SQL clause for a date or datetime field.
529 535 def date_range_clause(table, field, from, to)
530 536 s = []
531 537 if from
532 538 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date((Date.yesterday + from).to_time.end_of_day)])
533 539 end
534 540 if to
535 541 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date((Date.today + to).to_time.end_of_day)])
536 542 end
537 543 s.join(' AND ')
538 544 end
539 545 end
@@ -1,77 +1,86
1 1 <div class="contextual">
2 2 <% if !@query.new_record? && @query.editable_by?(User.current) %>
3 3 <%= link_to l(:button_edit), {:controller => 'queries', :action => 'edit', :id => @query}, :class => 'icon icon-edit' %>
4 4 <%= link_to l(:button_delete), {:controller => 'queries', :action => 'destroy', :id => @query}, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-del' %>
5 5 <% end %>
6 6 </div>
7 7
8 8 <h2><%= @query.new_record? ? l(:label_issue_plural) : h(@query.name) %></h2>
9 9 <% html_title(@query.new_record? ? l(:label_issue_plural) : @query.name) %>
10 10
11 11 <% form_tag({ :controller => 'queries', :action => 'new' }, :id => 'query_form') do %>
12 12 <%= hidden_field_tag('project_id', @project.to_param) if @project %>
13 13 <div id="query_form_content">
14 14 <fieldset id="filters" class="collapsible">
15 15 <legend onclick="toggleFieldset(this);"><%= l(:label_filter_plural) %></legend>
16 16 <div>
17 17 <%= render :partial => 'queries/filters', :locals => {:query => @query} %>
18 18 </div>
19 19 </fieldset>
20 20 <fieldset class="collapsible collapsed">
21 21 <legend onclick="toggleFieldset(this);"><%= l(:label_options) %></legend>
22 22 <div style="display: none;">
23 <%= l(:field_group_by) %>
24 <%= select_tag('group_by', options_for_select([[]] + @query.groupable_columns.collect {|c| [c.caption, c.name.to_s]}, @query.group_by)) %>
23 <table>
24 <tr>
25 <td><%= l(:field_column_names) %></td>
26 <td><%= render :partial => 'queries/columns', :locals => {:query => @query} %></td>
27 </tr>
28 <tr>
29 <td><%= l(:field_group_by) %></td>
30 <td><%= select_tag('group_by', options_for_select([[]] + @query.groupable_columns.collect {|c| [c.caption, c.name.to_s]}, @query.group_by)) %></td>
31 </tr>
32 </table>
25 33 </div>
26 34 </fieldset>
27 35 </div>
28 36 <p class="buttons">
29 37
30 38 <%= link_to_remote l(:button_apply),
31 39 { :url => { :set_filter => 1 },
40 :before => 'selectAllOptions("selected_columns");',
32 41 :update => "content",
33 42 :with => "Form.serialize('query_form')"
34 43 }, :class => 'icon icon-checked' %>
35 44
36 45 <%= link_to_remote l(:button_clear),
37 46 { :url => { :set_filter => 1, :project_id => @project },
38 47 :method => :get,
39 48 :update => "content",
40 49 }, :class => 'icon icon-reload' %>
41 50
42 51 <% if @query.new_record? && User.current.allowed_to?(:save_queries, @project, :global => true) %>
43 <%= link_to l(:button_save), {}, :onclick => "$('query_form').submit(); return false;", :class => 'icon icon-save' %>
52 <%= link_to l(:button_save), {}, :onclick => "selectAllOptions('selected_columns'); $('query_form').submit(); return false;", :class => 'icon icon-save' %>
44 53 <% end %>
45 54 </p>
46 55 <% end %>
47 56
48 57 <%= error_messages_for 'query' %>
49 58 <% if @query.valid? %>
50 59 <% if @issues.empty? %>
51 60 <p class="nodata"><%= l(:label_no_data) %></p>
52 61 <% else %>
53 62 <%= render :partial => 'issues/list', :locals => {:issues => @issues, :query => @query} %>
54 63 <p class="pagination"><%= pagination_links_full @issue_pages, @issue_count %></p>
55 64 <% end %>
56 65
57 66 <% other_formats_links do |f| %>
58 67 <%= f.link_to 'Atom', :url => { :project_id => @project, :query_id => (@query.new_record? ? nil : @query), :key => User.current.rss_key } %>
59 68 <%= f.link_to 'CSV', :url => { :project_id => @project } %>
60 69 <%= f.link_to 'PDF', :url => { :project_id => @project } %>
61 70 <% end %>
62 71
63 72 <% end %>
64 73
65 74 <% content_for :sidebar do %>
66 75 <%= render :partial => 'issues/sidebar' %>
67 76 <% end %>
68 77
69 78 <% content_for :header_tags do %>
70 79 <%= auto_discovery_link_tag(:atom, {:query_id => @query, :format => 'atom', :page => nil, :key => User.current.rss_key}, :title => l(:label_issue_plural)) %>
71 80 <%= auto_discovery_link_tag(:atom, {:action => 'changes', :query_id => @query, :format => 'atom', :page => nil, :key => User.current.rss_key}, :title => l(:label_changes_details)) %>
72 81 <%= javascript_include_tag 'context_menu' %>
73 82 <%= stylesheet_link_tag 'context_menu' %>
74 83 <% end %>
75 84
76 85 <div id="context-menu" style="display: none;"></div>
77 86 <%= javascript_tag "new ContextMenu('#{url_for(:controller => 'issues', :action => 'context_menu')}')" %>
@@ -1,27 +1,22
1 <% content_tag 'fieldset', :id => 'columns', :style => (query.has_default_columns? ? 'display:none;' : nil) do %>
2 <legend><%= l(:field_column_names) %></legend>
3
4 <%= hidden_field_tag 'query[column_names][]', '', :id => nil %>
5 <table>
1 <table style="border-collapse: collapse; border:0;">
6 2 <tr>
7 <td><%= select_tag 'available_columns',
3 <td style="padding-left:0"><%= select_tag 'available_columns',
8 4 options_for_select((query.available_columns - query.columns).collect {|column| [column.caption, column.name]}),
9 5 :multiple => true, :size => 10, :style => "width:150px" %>
10 6 </td>
11 7 <td align="center" valign="middle">
12 8 <input type="button" value="--&gt;"
13 9 onclick="moveOptions(this.form.available_columns, this.form.selected_columns);" /><br />
14 10 <input type="button" value="&lt;--"
15 11 onclick="moveOptions(this.form.selected_columns, this.form.available_columns);" />
16 12 </td>
17 13 <td><%= select_tag 'query[column_names][]',
18 options_for_select(@query.columns.collect {|column| [column.caption, column.name]}),
14 options_for_select(query.columns.collect {|column| [column.caption, column.name]}),
19 15 :id => 'selected_columns', :multiple => true, :size => 10, :style => "width:150px" %>
20 16 </td>
21 17 </tr>
22 18 </table>
23 <% end %>
24 19
25 20 <% content_for :header_tags do %>
26 21 <%= javascript_include_tag 'select_list_move' %>
27 22 <% end %>
@@ -1,41 +1,45
1 1 <%= error_messages_for 'query' %>
2 2 <%= hidden_field_tag 'confirm', 1 %>
3 3
4 4 <div class="box">
5 5 <div class="tabular">
6 6 <p><label for="query_name"><%=l(:field_name)%></label>
7 7 <%= text_field 'query', 'name', :size => 80 %></p>
8 8
9 9 <% if User.current.admin? || User.current.allowed_to?(:manage_public_queries, @project) %>
10 10 <p><label for="query_is_public"><%=l(:field_is_public)%></label>
11 11 <%= check_box 'query', 'is_public',
12 12 :onchange => (User.current.admin? ? nil : 'if (this.checked) {$("query_is_for_all").checked = false; $("query_is_for_all").disabled = true;} else {$("query_is_for_all").disabled = false;}') %></p>
13 13 <% end %>
14 14
15 15 <p><label for="query_is_for_all"><%=l(:field_is_for_all)%></label>
16 16 <%= check_box_tag 'query_is_for_all', 1, @query.project.nil?,
17 17 :disabled => (!@query.new_record? && (@query.project.nil? || (@query.is_public? && !User.current.admin?))) %></p>
18 18
19 19 <p><label for="query_default_columns"><%=l(:label_default_columns)%></label>
20 20 <%= check_box_tag 'default_columns', 1, @query.has_default_columns?, :id => 'query_default_columns',
21 21 :onclick => 'if (this.checked) {Element.hide("columns")} else {Element.show("columns")}' %></p>
22 22
23 23 <p><label for="query_group_by"><%= l(:field_group_by) %></label>
24 24 <%= select 'query', 'group_by', @query.groupable_columns.collect {|c| [c.caption, c.name.to_s]}, :include_blank => true %></p>
25 25 </div>
26 26
27 27 <fieldset><legend><%= l(:label_filter_plural) %></legend>
28 28 <%= render :partial => 'queries/filters', :locals => {:query => query}%>
29 29 </fieldset>
30 30
31 31 <fieldset><legend><%= l(:label_sort) %></legend>
32 32 <% 3.times do |i| %>
33 33 <%= i+1 %>: <%= select_tag("query[sort_criteria][#{i}][]",
34 34 options_for_select([[]] + query.available_columns.select(&:sortable?).collect {|column| [column.caption, column.name.to_s]}, @query.sort_criteria_key(i))) %>
35 35 <%= select_tag("query[sort_criteria][#{i}][]",
36 36 options_for_select([[], [l(:label_ascending), 'asc'], [l(:label_descending), 'desc']], @query.sort_criteria_order(i))) %><br />
37 37 <% end %>
38 38 </fieldset>
39 39
40 <% content_tag 'fieldset', :id => 'columns', :style => (query.has_default_columns? ? 'display:none;' : nil) do %>
41 <legend><%= l(:field_column_names) %></legend>
40 42 <%= render :partial => 'queries/columns', :locals => {:query => query}%>
43 <% end %>
44
41 45 </div>
@@ -1,1159 +1,1175
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2008 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.dirname(__FILE__) + '/../test_helper'
19 19 require 'issues_controller'
20 20
21 21 # Re-raise errors caught by the controller.
22 22 class IssuesController; def rescue_action(e) raise e end; end
23 23
24 24 class IssuesControllerTest < ActionController::TestCase
25 25 fixtures :projects,
26 26 :users,
27 27 :roles,
28 28 :members,
29 29 :member_roles,
30 30 :issues,
31 31 :issue_statuses,
32 32 :versions,
33 33 :trackers,
34 34 :projects_trackers,
35 35 :issue_categories,
36 36 :enabled_modules,
37 37 :enumerations,
38 38 :attachments,
39 39 :workflows,
40 40 :custom_fields,
41 41 :custom_values,
42 42 :custom_fields_projects,
43 43 :custom_fields_trackers,
44 44 :time_entries,
45 45 :journals,
46 46 :journal_details,
47 47 :queries
48 48
49 49 def setup
50 50 @controller = IssuesController.new
51 51 @request = ActionController::TestRequest.new
52 52 @response = ActionController::TestResponse.new
53 53 User.current = nil
54 54 end
55 55
56 56 def test_index_routing
57 57 assert_routing(
58 58 {:method => :get, :path => '/issues'},
59 59 :controller => 'issues', :action => 'index'
60 60 )
61 61 end
62 62
63 63 def test_index
64 64 Setting.default_language = 'en'
65 65
66 66 get :index
67 67 assert_response :success
68 68 assert_template 'index.rhtml'
69 69 assert_not_nil assigns(:issues)
70 70 assert_nil assigns(:project)
71 71 assert_tag :tag => 'a', :content => /Can't print recipes/
72 72 assert_tag :tag => 'a', :content => /Subproject issue/
73 73 # private projects hidden
74 74 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
75 75 assert_no_tag :tag => 'a', :content => /Issue on project 2/
76 76 # project column
77 77 assert_tag :tag => 'th', :content => /Project/
78 78 end
79 79
80 80 def test_index_should_not_list_issues_when_module_disabled
81 81 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
82 82 get :index
83 83 assert_response :success
84 84 assert_template 'index.rhtml'
85 85 assert_not_nil assigns(:issues)
86 86 assert_nil assigns(:project)
87 87 assert_no_tag :tag => 'a', :content => /Can't print recipes/
88 88 assert_tag :tag => 'a', :content => /Subproject issue/
89 89 end
90 90
91 91 def test_index_with_project_routing
92 92 assert_routing(
93 93 {:method => :get, :path => '/projects/23/issues'},
94 94 :controller => 'issues', :action => 'index', :project_id => '23'
95 95 )
96 96 end
97 97
98 98 def test_index_should_not_list_issues_when_module_disabled
99 99 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
100 100 get :index
101 101 assert_response :success
102 102 assert_template 'index.rhtml'
103 103 assert_not_nil assigns(:issues)
104 104 assert_nil assigns(:project)
105 105 assert_no_tag :tag => 'a', :content => /Can't print recipes/
106 106 assert_tag :tag => 'a', :content => /Subproject issue/
107 107 end
108 108
109 109 def test_index_with_project_routing
110 110 assert_routing(
111 111 {:method => :get, :path => 'projects/23/issues'},
112 112 :controller => 'issues', :action => 'index', :project_id => '23'
113 113 )
114 114 end
115 115
116 116 def test_index_with_project
117 117 Setting.display_subprojects_issues = 0
118 118 get :index, :project_id => 1
119 119 assert_response :success
120 120 assert_template 'index.rhtml'
121 121 assert_not_nil assigns(:issues)
122 122 assert_tag :tag => 'a', :content => /Can't print recipes/
123 123 assert_no_tag :tag => 'a', :content => /Subproject issue/
124 124 end
125 125
126 126 def test_index_with_project_and_subprojects
127 127 Setting.display_subprojects_issues = 1
128 128 get :index, :project_id => 1
129 129 assert_response :success
130 130 assert_template 'index.rhtml'
131 131 assert_not_nil assigns(:issues)
132 132 assert_tag :tag => 'a', :content => /Can't print recipes/
133 133 assert_tag :tag => 'a', :content => /Subproject issue/
134 134 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
135 135 end
136 136
137 137 def test_index_with_project_and_subprojects_should_show_private_subprojects
138 138 @request.session[:user_id] = 2
139 139 Setting.display_subprojects_issues = 1
140 140 get :index, :project_id => 1
141 141 assert_response :success
142 142 assert_template 'index.rhtml'
143 143 assert_not_nil assigns(:issues)
144 144 assert_tag :tag => 'a', :content => /Can't print recipes/
145 145 assert_tag :tag => 'a', :content => /Subproject issue/
146 146 assert_tag :tag => 'a', :content => /Issue of a private subproject/
147 147 end
148 148
149 149 def test_index_with_project_routing_formatted
150 150 assert_routing(
151 151 {:method => :get, :path => 'projects/23/issues.pdf'},
152 152 :controller => 'issues', :action => 'index', :project_id => '23', :format => 'pdf'
153 153 )
154 154 assert_routing(
155 155 {:method => :get, :path => 'projects/23/issues.atom'},
156 156 :controller => 'issues', :action => 'index', :project_id => '23', :format => 'atom'
157 157 )
158 158 end
159 159
160 160 def test_index_with_project_and_filter
161 161 get :index, :project_id => 1, :set_filter => 1
162 162 assert_response :success
163 163 assert_template 'index.rhtml'
164 164 assert_not_nil assigns(:issues)
165 165 end
166 166
167 167 def test_index_with_query
168 168 get :index, :project_id => 1, :query_id => 5
169 169 assert_response :success
170 170 assert_template 'index.rhtml'
171 171 assert_not_nil assigns(:issues)
172 172 assert_nil assigns(:issue_count_by_group)
173 173 end
174 174
175 175 def test_index_with_query_grouped_by_tracker
176 176 get :index, :project_id => 1, :query_id => 6
177 177 assert_response :success
178 178 assert_template 'index.rhtml'
179 179 assert_not_nil assigns(:issues)
180 180 count_by_group = assigns(:issue_count_by_group)
181 181 assert_kind_of Hash, count_by_group
182 182 assert_kind_of Tracker, count_by_group.keys.first
183 183 assert_not_nil count_by_group[Tracker.find(1)]
184 184 end
185 185
186 186 def test_index_with_query_grouped_by_list_custom_field
187 187 get :index, :project_id => 1, :query_id => 9
188 188 assert_response :success
189 189 assert_template 'index.rhtml'
190 190 assert_not_nil assigns(:issues)
191 191 count_by_group = assigns(:issue_count_by_group)
192 192 assert_kind_of Hash, count_by_group
193 193 assert_kind_of String, count_by_group.keys.first
194 194 assert_not_nil count_by_group['MySQL']
195 195 end
196 196
197 197 def test_index_sort_by_field_not_included_in_columns
198 198 Setting.issue_list_default_columns = %w(subject author)
199 199 get :index, :sort => 'tracker'
200 200 end
201 201
202 202 def test_index_csv_with_project
203 203 Setting.default_language = 'en'
204 204
205 205 get :index, :format => 'csv'
206 206 assert_response :success
207 207 assert_not_nil assigns(:issues)
208 208 assert_equal 'text/csv', @response.content_type
209 209 assert @response.body.starts_with?("#,")
210 210
211 211 get :index, :project_id => 1, :format => 'csv'
212 212 assert_response :success
213 213 assert_not_nil assigns(:issues)
214 214 assert_equal 'text/csv', @response.content_type
215 215 end
216 216
217 217 def test_index_formatted
218 218 assert_routing(
219 219 {:method => :get, :path => 'issues.pdf'},
220 220 :controller => 'issues', :action => 'index', :format => 'pdf'
221 221 )
222 222 assert_routing(
223 223 {:method => :get, :path => 'issues.atom'},
224 224 :controller => 'issues', :action => 'index', :format => 'atom'
225 225 )
226 226 end
227 227
228 228 def test_index_pdf
229 229 get :index, :format => 'pdf'
230 230 assert_response :success
231 231 assert_not_nil assigns(:issues)
232 232 assert_equal 'application/pdf', @response.content_type
233 233
234 234 get :index, :project_id => 1, :format => 'pdf'
235 235 assert_response :success
236 236 assert_not_nil assigns(:issues)
237 237 assert_equal 'application/pdf', @response.content_type
238 238
239 239 get :index, :project_id => 1, :query_id => 6, :format => 'pdf'
240 240 assert_response :success
241 241 assert_not_nil assigns(:issues)
242 242 assert_equal 'application/pdf', @response.content_type
243 243 end
244 244
245 245 def test_index_sort
246 246 get :index, :sort => 'tracker,id:desc'
247 247 assert_response :success
248 248
249 249 sort_params = @request.session['issues_index_sort']
250 250 assert sort_params.is_a?(String)
251 251 assert_equal 'tracker,id:desc', sort_params
252 252
253 253 issues = assigns(:issues)
254 254 assert_not_nil issues
255 255 assert !issues.empty?
256 256 assert_equal issues.sort {|a,b| a.tracker == b.tracker ? b.id <=> a.id : a.tracker <=> b.tracker }.collect(&:id), issues.collect(&:id)
257 257 end
258 258
259 def test_index_with_columns
260 columns = ['tracker', 'subject', 'assigned_to']
261 get :index, :set_filter => 1, :query => { 'column_names' => columns}
262 assert_response :success
263
264 # query should use specified columns
265 query = assigns(:query)
266 assert_kind_of Query, query
267 assert_equal columns, query.column_names.map(&:to_s)
268
269 # columns should be stored in session
270 assert_kind_of Hash, session[:query]
271 assert_kind_of Array, session[:query][:column_names]
272 assert_equal columns, session[:query][:column_names].map(&:to_s)
273 end
274
259 275 def test_gantt
260 276 get :gantt, :project_id => 1
261 277 assert_response :success
262 278 assert_template 'gantt.rhtml'
263 279 assert_not_nil assigns(:gantt)
264 280 events = assigns(:gantt).events
265 281 assert_not_nil events
266 282 # Issue with start and due dates
267 283 i = Issue.find(1)
268 284 assert_not_nil i.due_date
269 285 assert events.include?(Issue.find(1))
270 286 # Issue with without due date but targeted to a version with date
271 287 i = Issue.find(2)
272 288 assert_nil i.due_date
273 289 assert events.include?(i)
274 290 end
275 291
276 292 def test_cross_project_gantt
277 293 get :gantt
278 294 assert_response :success
279 295 assert_template 'gantt.rhtml'
280 296 assert_not_nil assigns(:gantt)
281 297 events = assigns(:gantt).events
282 298 assert_not_nil events
283 299 end
284 300
285 301 def test_gantt_export_to_pdf
286 302 get :gantt, :project_id => 1, :format => 'pdf'
287 303 assert_response :success
288 304 assert_equal 'application/pdf', @response.content_type
289 305 assert @response.body.starts_with?('%PDF')
290 306 assert_not_nil assigns(:gantt)
291 307 end
292 308
293 309 def test_cross_project_gantt_export_to_pdf
294 310 get :gantt, :format => 'pdf'
295 311 assert_response :success
296 312 assert_equal 'application/pdf', @response.content_type
297 313 assert @response.body.starts_with?('%PDF')
298 314 assert_not_nil assigns(:gantt)
299 315 end
300 316
301 317 if Object.const_defined?(:Magick)
302 318 def test_gantt_image
303 319 get :gantt, :project_id => 1, :format => 'png'
304 320 assert_response :success
305 321 assert_equal 'image/png', @response.content_type
306 322 end
307 323 else
308 324 puts "RMagick not installed. Skipping tests !!!"
309 325 end
310 326
311 327 def test_calendar
312 328 get :calendar, :project_id => 1
313 329 assert_response :success
314 330 assert_template 'calendar'
315 331 assert_not_nil assigns(:calendar)
316 332 end
317 333
318 334 def test_cross_project_calendar
319 335 get :calendar
320 336 assert_response :success
321 337 assert_template 'calendar'
322 338 assert_not_nil assigns(:calendar)
323 339 end
324 340
325 341 def test_changes
326 342 get :changes, :project_id => 1
327 343 assert_response :success
328 344 assert_not_nil assigns(:journals)
329 345 assert_equal 'application/atom+xml', @response.content_type
330 346 end
331 347
332 348 def test_show_routing
333 349 assert_routing(
334 350 {:method => :get, :path => '/issues/64'},
335 351 :controller => 'issues', :action => 'show', :id => '64'
336 352 )
337 353 end
338 354
339 355 def test_show_routing_formatted
340 356 assert_routing(
341 357 {:method => :get, :path => '/issues/2332.pdf'},
342 358 :controller => 'issues', :action => 'show', :id => '2332', :format => 'pdf'
343 359 )
344 360 assert_routing(
345 361 {:method => :get, :path => '/issues/23123.atom'},
346 362 :controller => 'issues', :action => 'show', :id => '23123', :format => 'atom'
347 363 )
348 364 end
349 365
350 366 def test_show_by_anonymous
351 367 get :show, :id => 1
352 368 assert_response :success
353 369 assert_template 'show.rhtml'
354 370 assert_not_nil assigns(:issue)
355 371 assert_equal Issue.find(1), assigns(:issue)
356 372
357 373 # anonymous role is allowed to add a note
358 374 assert_tag :tag => 'form',
359 375 :descendant => { :tag => 'fieldset',
360 376 :child => { :tag => 'legend',
361 377 :content => /Notes/ } }
362 378 end
363 379
364 380 def test_show_by_manager
365 381 @request.session[:user_id] = 2
366 382 get :show, :id => 1
367 383 assert_response :success
368 384
369 385 assert_tag :tag => 'form',
370 386 :descendant => { :tag => 'fieldset',
371 387 :child => { :tag => 'legend',
372 388 :content => /Change properties/ } },
373 389 :descendant => { :tag => 'fieldset',
374 390 :child => { :tag => 'legend',
375 391 :content => /Log time/ } },
376 392 :descendant => { :tag => 'fieldset',
377 393 :child => { :tag => 'legend',
378 394 :content => /Notes/ } }
379 395 end
380 396
381 397 def test_show_should_deny_anonymous_access_without_permission
382 398 Role.anonymous.remove_permission!(:view_issues)
383 399 get :show, :id => 1
384 400 assert_response :redirect
385 401 end
386 402
387 403 def test_show_should_deny_non_member_access_without_permission
388 404 Role.non_member.remove_permission!(:view_issues)
389 405 @request.session[:user_id] = 9
390 406 get :show, :id => 1
391 407 assert_response 403
392 408 end
393 409
394 410 def test_show_should_deny_member_access_without_permission
395 411 Role.find(1).remove_permission!(:view_issues)
396 412 @request.session[:user_id] = 2
397 413 get :show, :id => 1
398 414 assert_response 403
399 415 end
400 416
401 417 def test_show_should_not_disclose_relations_to_invisible_issues
402 418 Setting.cross_project_issue_relations = '1'
403 419 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(2), :relation_type => 'relates')
404 420 # Relation to a private project issue
405 421 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(4), :relation_type => 'relates')
406 422
407 423 get :show, :id => 1
408 424 assert_response :success
409 425
410 426 assert_tag :div, :attributes => { :id => 'relations' },
411 427 :descendant => { :tag => 'a', :content => /#2$/ }
412 428 assert_no_tag :div, :attributes => { :id => 'relations' },
413 429 :descendant => { :tag => 'a', :content => /#4$/ }
414 430 end
415 431
416 432 def test_show_atom
417 433 get :show, :id => 2, :format => 'atom'
418 434 assert_response :success
419 435 assert_template 'changes.rxml'
420 436 # Inline image
421 437 assert @response.body.include?("&lt;img src=\"http://test.host/attachments/download/10\" alt=\"\" /&gt;"), "Body did not match. Body: #{@response.body}"
422 438 end
423 439
424 440 def test_new_routing
425 441 assert_routing(
426 442 {:method => :get, :path => '/projects/1/issues/new'},
427 443 :controller => 'issues', :action => 'new', :project_id => '1'
428 444 )
429 445 assert_recognizes(
430 446 {:controller => 'issues', :action => 'new', :project_id => '1'},
431 447 {:method => :post, :path => '/projects/1/issues'}
432 448 )
433 449 end
434 450
435 451 def test_show_export_to_pdf
436 452 get :show, :id => 3, :format => 'pdf'
437 453 assert_response :success
438 454 assert_equal 'application/pdf', @response.content_type
439 455 assert @response.body.starts_with?('%PDF')
440 456 assert_not_nil assigns(:issue)
441 457 end
442 458
443 459 def test_get_new
444 460 @request.session[:user_id] = 2
445 461 get :new, :project_id => 1, :tracker_id => 1
446 462 assert_response :success
447 463 assert_template 'new'
448 464
449 465 assert_tag :tag => 'input', :attributes => { :name => 'issue[custom_field_values][2]',
450 466 :value => 'Default string' }
451 467 end
452 468
453 469 def test_get_new_without_tracker_id
454 470 @request.session[:user_id] = 2
455 471 get :new, :project_id => 1
456 472 assert_response :success
457 473 assert_template 'new'
458 474
459 475 issue = assigns(:issue)
460 476 assert_not_nil issue
461 477 assert_equal Project.find(1).trackers.first, issue.tracker
462 478 end
463 479
464 480 def test_get_new_with_no_default_status_should_display_an_error
465 481 @request.session[:user_id] = 2
466 482 IssueStatus.delete_all
467 483
468 484 get :new, :project_id => 1
469 485 assert_response 500
470 486 assert_not_nil flash[:error]
471 487 assert_tag :tag => 'div', :attributes => { :class => /error/ },
472 488 :content => /No default issue/
473 489 end
474 490
475 491 def test_get_new_with_no_tracker_should_display_an_error
476 492 @request.session[:user_id] = 2
477 493 Tracker.delete_all
478 494
479 495 get :new, :project_id => 1
480 496 assert_response 500
481 497 assert_not_nil flash[:error]
482 498 assert_tag :tag => 'div', :attributes => { :class => /error/ },
483 499 :content => /No tracker/
484 500 end
485 501
486 502 def test_update_new_form
487 503 @request.session[:user_id] = 2
488 504 xhr :post, :new, :project_id => 1,
489 505 :issue => {:tracker_id => 2,
490 506 :subject => 'This is the test_new issue',
491 507 :description => 'This is the description',
492 508 :priority_id => 5}
493 509 assert_response :success
494 510 assert_template 'new'
495 511 end
496 512
497 513 def test_post_new
498 514 @request.session[:user_id] = 2
499 515 assert_difference 'Issue.count' do
500 516 post :new, :project_id => 1,
501 517 :issue => {:tracker_id => 3,
502 518 :subject => 'This is the test_new issue',
503 519 :description => 'This is the description',
504 520 :priority_id => 5,
505 521 :estimated_hours => '',
506 522 :custom_field_values => {'2' => 'Value for field 2'}}
507 523 end
508 524 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
509 525
510 526 issue = Issue.find_by_subject('This is the test_new issue')
511 527 assert_not_nil issue
512 528 assert_equal 2, issue.author_id
513 529 assert_equal 3, issue.tracker_id
514 530 assert_nil issue.estimated_hours
515 531 v = issue.custom_values.find(:first, :conditions => {:custom_field_id => 2})
516 532 assert_not_nil v
517 533 assert_equal 'Value for field 2', v.value
518 534 end
519 535
520 536 def test_post_new_and_continue
521 537 @request.session[:user_id] = 2
522 538 post :new, :project_id => 1,
523 539 :issue => {:tracker_id => 3,
524 540 :subject => 'This is first issue',
525 541 :priority_id => 5},
526 542 :continue => ''
527 543 assert_redirected_to :controller => 'issues', :action => 'new', :tracker_id => 3
528 544 end
529 545
530 546 def test_post_new_without_custom_fields_param
531 547 @request.session[:user_id] = 2
532 548 assert_difference 'Issue.count' do
533 549 post :new, :project_id => 1,
534 550 :issue => {:tracker_id => 1,
535 551 :subject => 'This is the test_new issue',
536 552 :description => 'This is the description',
537 553 :priority_id => 5}
538 554 end
539 555 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
540 556 end
541 557
542 558 def test_post_new_with_required_custom_field_and_without_custom_fields_param
543 559 field = IssueCustomField.find_by_name('Database')
544 560 field.update_attribute(:is_required, true)
545 561
546 562 @request.session[:user_id] = 2
547 563 post :new, :project_id => 1,
548 564 :issue => {:tracker_id => 1,
549 565 :subject => 'This is the test_new issue',
550 566 :description => 'This is the description',
551 567 :priority_id => 5}
552 568 assert_response :success
553 569 assert_template 'new'
554 570 issue = assigns(:issue)
555 571 assert_not_nil issue
556 572 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
557 573 end
558 574
559 575 def test_post_new_with_watchers
560 576 @request.session[:user_id] = 2
561 577 ActionMailer::Base.deliveries.clear
562 578
563 579 assert_difference 'Watcher.count', 2 do
564 580 post :new, :project_id => 1,
565 581 :issue => {:tracker_id => 1,
566 582 :subject => 'This is a new issue with watchers',
567 583 :description => 'This is the description',
568 584 :priority_id => 5,
569 585 :watcher_user_ids => ['2', '3']}
570 586 end
571 587 issue = Issue.find_by_subject('This is a new issue with watchers')
572 588 assert_not_nil issue
573 589 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
574 590
575 591 # Watchers added
576 592 assert_equal [2, 3], issue.watcher_user_ids.sort
577 593 assert issue.watched_by?(User.find(3))
578 594 # Watchers notified
579 595 mail = ActionMailer::Base.deliveries.last
580 596 assert_kind_of TMail::Mail, mail
581 597 assert [mail.bcc, mail.cc].flatten.include?(User.find(3).mail)
582 598 end
583 599
584 600 def test_post_new_should_send_a_notification
585 601 ActionMailer::Base.deliveries.clear
586 602 @request.session[:user_id] = 2
587 603 assert_difference 'Issue.count' do
588 604 post :new, :project_id => 1,
589 605 :issue => {:tracker_id => 3,
590 606 :subject => 'This is the test_new issue',
591 607 :description => 'This is the description',
592 608 :priority_id => 5,
593 609 :estimated_hours => '',
594 610 :custom_field_values => {'2' => 'Value for field 2'}}
595 611 end
596 612 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
597 613
598 614 assert_equal 1, ActionMailer::Base.deliveries.size
599 615 end
600 616
601 617 def test_post_should_preserve_fields_values_on_validation_failure
602 618 @request.session[:user_id] = 2
603 619 post :new, :project_id => 1,
604 620 :issue => {:tracker_id => 1,
605 621 # empty subject
606 622 :subject => '',
607 623 :description => 'This is a description',
608 624 :priority_id => 6,
609 625 :custom_field_values => {'1' => 'Oracle', '2' => 'Value for field 2'}}
610 626 assert_response :success
611 627 assert_template 'new'
612 628
613 629 assert_tag :textarea, :attributes => { :name => 'issue[description]' },
614 630 :content => 'This is a description'
615 631 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
616 632 :child => { :tag => 'option', :attributes => { :selected => 'selected',
617 633 :value => '6' },
618 634 :content => 'High' }
619 635 # Custom fields
620 636 assert_tag :select, :attributes => { :name => 'issue[custom_field_values][1]' },
621 637 :child => { :tag => 'option', :attributes => { :selected => 'selected',
622 638 :value => 'Oracle' },
623 639 :content => 'Oracle' }
624 640 assert_tag :input, :attributes => { :name => 'issue[custom_field_values][2]',
625 641 :value => 'Value for field 2'}
626 642 end
627 643
628 644 def test_copy_routing
629 645 assert_routing(
630 646 {:method => :get, :path => '/projects/world_domination/issues/567/copy'},
631 647 :controller => 'issues', :action => 'new', :project_id => 'world_domination', :copy_from => '567'
632 648 )
633 649 end
634 650
635 651 def test_copy_issue
636 652 @request.session[:user_id] = 2
637 653 get :new, :project_id => 1, :copy_from => 1
638 654 assert_template 'new'
639 655 assert_not_nil assigns(:issue)
640 656 orig = Issue.find(1)
641 657 assert_equal orig.subject, assigns(:issue).subject
642 658 end
643 659
644 660 def test_edit_routing
645 661 assert_routing(
646 662 {:method => :get, :path => '/issues/1/edit'},
647 663 :controller => 'issues', :action => 'edit', :id => '1'
648 664 )
649 665 assert_recognizes( #TODO: use a PUT on the issue URI isntead, need to adjust form
650 666 {:controller => 'issues', :action => 'edit', :id => '1'},
651 667 {:method => :post, :path => '/issues/1/edit'}
652 668 )
653 669 end
654 670
655 671 def test_get_edit
656 672 @request.session[:user_id] = 2
657 673 get :edit, :id => 1
658 674 assert_response :success
659 675 assert_template 'edit'
660 676 assert_not_nil assigns(:issue)
661 677 assert_equal Issue.find(1), assigns(:issue)
662 678 end
663 679
664 680 def test_get_edit_with_params
665 681 @request.session[:user_id] = 2
666 682 get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 }
667 683 assert_response :success
668 684 assert_template 'edit'
669 685
670 686 issue = assigns(:issue)
671 687 assert_not_nil issue
672 688
673 689 assert_equal 5, issue.status_id
674 690 assert_tag :select, :attributes => { :name => 'issue[status_id]' },
675 691 :child => { :tag => 'option',
676 692 :content => 'Closed',
677 693 :attributes => { :selected => 'selected' } }
678 694
679 695 assert_equal 7, issue.priority_id
680 696 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
681 697 :child => { :tag => 'option',
682 698 :content => 'Urgent',
683 699 :attributes => { :selected => 'selected' } }
684 700 end
685 701
686 702 def test_reply_routing
687 703 assert_routing(
688 704 {:method => :post, :path => '/issues/1/quoted'},
689 705 :controller => 'issues', :action => 'reply', :id => '1'
690 706 )
691 707 end
692 708
693 709 def test_reply_to_issue
694 710 @request.session[:user_id] = 2
695 711 get :reply, :id => 1
696 712 assert_response :success
697 713 assert_select_rjs :show, "update"
698 714 end
699 715
700 716 def test_reply_to_note
701 717 @request.session[:user_id] = 2
702 718 get :reply, :id => 1, :journal_id => 2
703 719 assert_response :success
704 720 assert_select_rjs :show, "update"
705 721 end
706 722
707 723 def test_post_edit_without_custom_fields_param
708 724 @request.session[:user_id] = 2
709 725 ActionMailer::Base.deliveries.clear
710 726
711 727 issue = Issue.find(1)
712 728 assert_equal '125', issue.custom_value_for(2).value
713 729 old_subject = issue.subject
714 730 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
715 731
716 732 assert_difference('Journal.count') do
717 733 assert_difference('JournalDetail.count', 2) do
718 734 post :edit, :id => 1, :issue => {:subject => new_subject,
719 735 :priority_id => '6',
720 736 :category_id => '1' # no change
721 737 }
722 738 end
723 739 end
724 740 assert_redirected_to :action => 'show', :id => '1'
725 741 issue.reload
726 742 assert_equal new_subject, issue.subject
727 743 # Make sure custom fields were not cleared
728 744 assert_equal '125', issue.custom_value_for(2).value
729 745
730 746 mail = ActionMailer::Base.deliveries.last
731 747 assert_kind_of TMail::Mail, mail
732 748 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
733 749 assert mail.body.include?("Subject changed from #{old_subject} to #{new_subject}")
734 750 end
735 751
736 752 def test_post_edit_with_custom_field_change
737 753 @request.session[:user_id] = 2
738 754 issue = Issue.find(1)
739 755 assert_equal '125', issue.custom_value_for(2).value
740 756
741 757 assert_difference('Journal.count') do
742 758 assert_difference('JournalDetail.count', 3) do
743 759 post :edit, :id => 1, :issue => {:subject => 'Custom field change',
744 760 :priority_id => '6',
745 761 :category_id => '1', # no change
746 762 :custom_field_values => { '2' => 'New custom value' }
747 763 }
748 764 end
749 765 end
750 766 assert_redirected_to :action => 'show', :id => '1'
751 767 issue.reload
752 768 assert_equal 'New custom value', issue.custom_value_for(2).value
753 769
754 770 mail = ActionMailer::Base.deliveries.last
755 771 assert_kind_of TMail::Mail, mail
756 772 assert mail.body.include?("Searchable field changed from 125 to New custom value")
757 773 end
758 774
759 775 def test_post_edit_with_status_and_assignee_change
760 776 issue = Issue.find(1)
761 777 assert_equal 1, issue.status_id
762 778 @request.session[:user_id] = 2
763 779 assert_difference('TimeEntry.count', 0) do
764 780 post :edit,
765 781 :id => 1,
766 782 :issue => { :status_id => 2, :assigned_to_id => 3 },
767 783 :notes => 'Assigned to dlopper',
768 784 :time_entry => { :hours => '', :comments => '', :activity_id => TimeEntryActivity.first }
769 785 end
770 786 assert_redirected_to :action => 'show', :id => '1'
771 787 issue.reload
772 788 assert_equal 2, issue.status_id
773 789 j = issue.journals.find(:first, :order => 'id DESC')
774 790 assert_equal 'Assigned to dlopper', j.notes
775 791 assert_equal 2, j.details.size
776 792
777 793 mail = ActionMailer::Base.deliveries.last
778 794 assert mail.body.include?("Status changed from New to Assigned")
779 795 # subject should contain the new status
780 796 assert mail.subject.include?("(#{ IssueStatus.find(2).name })")
781 797 end
782 798
783 799 def test_post_edit_with_note_only
784 800 notes = 'Note added by IssuesControllerTest#test_update_with_note_only'
785 801 # anonymous user
786 802 post :edit,
787 803 :id => 1,
788 804 :notes => notes
789 805 assert_redirected_to :action => 'show', :id => '1'
790 806 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
791 807 assert_equal notes, j.notes
792 808 assert_equal 0, j.details.size
793 809 assert_equal User.anonymous, j.user
794 810
795 811 mail = ActionMailer::Base.deliveries.last
796 812 assert mail.body.include?(notes)
797 813 end
798 814
799 815 def test_post_edit_with_note_and_spent_time
800 816 @request.session[:user_id] = 2
801 817 spent_hours_before = Issue.find(1).spent_hours
802 818 assert_difference('TimeEntry.count') do
803 819 post :edit,
804 820 :id => 1,
805 821 :notes => '2.5 hours added',
806 822 :time_entry => { :hours => '2.5', :comments => '', :activity_id => TimeEntryActivity.first }
807 823 end
808 824 assert_redirected_to :action => 'show', :id => '1'
809 825
810 826 issue = Issue.find(1)
811 827
812 828 j = issue.journals.find(:first, :order => 'id DESC')
813 829 assert_equal '2.5 hours added', j.notes
814 830 assert_equal 0, j.details.size
815 831
816 832 t = issue.time_entries.find(:first, :order => 'id DESC')
817 833 assert_not_nil t
818 834 assert_equal 2.5, t.hours
819 835 assert_equal spent_hours_before + 2.5, issue.spent_hours
820 836 end
821 837
822 838 def test_post_edit_with_attachment_only
823 839 set_tmp_attachments_directory
824 840
825 841 # Delete all fixtured journals, a race condition can occur causing the wrong
826 842 # journal to get fetched in the next find.
827 843 Journal.delete_all
828 844
829 845 # anonymous user
830 846 post :edit,
831 847 :id => 1,
832 848 :notes => '',
833 849 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
834 850 assert_redirected_to :action => 'show', :id => '1'
835 851 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
836 852 assert j.notes.blank?
837 853 assert_equal 1, j.details.size
838 854 assert_equal 'testfile.txt', j.details.first.value
839 855 assert_equal User.anonymous, j.user
840 856
841 857 mail = ActionMailer::Base.deliveries.last
842 858 assert mail.body.include?('testfile.txt')
843 859 end
844 860
845 861 def test_post_edit_with_no_change
846 862 issue = Issue.find(1)
847 863 issue.journals.clear
848 864 ActionMailer::Base.deliveries.clear
849 865
850 866 post :edit,
851 867 :id => 1,
852 868 :notes => ''
853 869 assert_redirected_to :action => 'show', :id => '1'
854 870
855 871 issue.reload
856 872 assert issue.journals.empty?
857 873 # No email should be sent
858 874 assert ActionMailer::Base.deliveries.empty?
859 875 end
860 876
861 877 def test_post_edit_should_send_a_notification
862 878 @request.session[:user_id] = 2
863 879 ActionMailer::Base.deliveries.clear
864 880 issue = Issue.find(1)
865 881 old_subject = issue.subject
866 882 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
867 883
868 884 post :edit, :id => 1, :issue => {:subject => new_subject,
869 885 :priority_id => '6',
870 886 :category_id => '1' # no change
871 887 }
872 888 assert_equal 1, ActionMailer::Base.deliveries.size
873 889 end
874 890
875 891 def test_post_edit_with_invalid_spent_time
876 892 @request.session[:user_id] = 2
877 893 notes = 'Note added by IssuesControllerTest#test_post_edit_with_invalid_spent_time'
878 894
879 895 assert_no_difference('Journal.count') do
880 896 post :edit,
881 897 :id => 1,
882 898 :notes => notes,
883 899 :time_entry => {"comments"=>"", "activity_id"=>"", "hours"=>"2z"}
884 900 end
885 901 assert_response :success
886 902 assert_template 'edit'
887 903
888 904 assert_tag :textarea, :attributes => { :name => 'notes' },
889 905 :content => notes
890 906 assert_tag :input, :attributes => { :name => 'time_entry[hours]', :value => "2z" }
891 907 end
892 908
893 909 def test_get_bulk_edit
894 910 @request.session[:user_id] = 2
895 911 get :bulk_edit, :ids => [1, 2]
896 912 assert_response :success
897 913 assert_template 'bulk_edit'
898 914 end
899 915
900 916 def test_bulk_edit
901 917 @request.session[:user_id] = 2
902 918 # update issues priority
903 919 post :bulk_edit, :ids => [1, 2], :priority_id => 7,
904 920 :assigned_to_id => '',
905 921 :custom_field_values => {'2' => ''},
906 922 :notes => 'Bulk editing'
907 923 assert_response 302
908 924 # check that the issues were updated
909 925 assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id}
910 926
911 927 issue = Issue.find(1)
912 928 journal = issue.journals.find(:first, :order => 'created_on DESC')
913 929 assert_equal '125', issue.custom_value_for(2).value
914 930 assert_equal 'Bulk editing', journal.notes
915 931 assert_equal 1, journal.details.size
916 932 end
917 933
918 934 def test_bullk_edit_should_send_a_notification
919 935 @request.session[:user_id] = 2
920 936 ActionMailer::Base.deliveries.clear
921 937 post(:bulk_edit,
922 938 {
923 939 :ids => [1, 2],
924 940 :priority_id => 7,
925 941 :assigned_to_id => '',
926 942 :custom_field_values => {'2' => ''},
927 943 :notes => 'Bulk editing'
928 944 })
929 945
930 946 assert_response 302
931 947 assert_equal 2, ActionMailer::Base.deliveries.size
932 948 end
933 949
934 950 def test_bulk_edit_status
935 951 @request.session[:user_id] = 2
936 952 # update issues priority
937 953 post :bulk_edit, :ids => [1, 2], :priority_id => '',
938 954 :assigned_to_id => '',
939 955 :status_id => '5',
940 956 :notes => 'Bulk editing status'
941 957 assert_response 302
942 958 issue = Issue.find(1)
943 959 assert issue.closed?
944 960 end
945 961
946 962 def test_bulk_edit_custom_field
947 963 @request.session[:user_id] = 2
948 964 # update issues priority
949 965 post :bulk_edit, :ids => [1, 2], :priority_id => '',
950 966 :assigned_to_id => '',
951 967 :custom_field_values => {'2' => '777'},
952 968 :notes => 'Bulk editing custom field'
953 969 assert_response 302
954 970
955 971 issue = Issue.find(1)
956 972 journal = issue.journals.find(:first, :order => 'created_on DESC')
957 973 assert_equal '777', issue.custom_value_for(2).value
958 974 assert_equal 1, journal.details.size
959 975 assert_equal '125', journal.details.first.old_value
960 976 assert_equal '777', journal.details.first.value
961 977 end
962 978
963 979 def test_bulk_unassign
964 980 assert_not_nil Issue.find(2).assigned_to
965 981 @request.session[:user_id] = 2
966 982 # unassign issues
967 983 post :bulk_edit, :ids => [1, 2], :notes => 'Bulk unassigning', :assigned_to_id => 'none'
968 984 assert_response 302
969 985 # check that the issues were updated
970 986 assert_nil Issue.find(2).assigned_to
971 987 end
972 988
973 989 def test_move_routing
974 990 assert_routing(
975 991 {:method => :get, :path => '/issues/1/move'},
976 992 :controller => 'issues', :action => 'move', :id => '1'
977 993 )
978 994 assert_recognizes(
979 995 {:controller => 'issues', :action => 'move', :id => '1'},
980 996 {:method => :post, :path => '/issues/1/move'}
981 997 )
982 998 end
983 999
984 1000 def test_move_one_issue_to_another_project
985 1001 @request.session[:user_id] = 2
986 1002 post :move, :id => 1, :new_project_id => 2
987 1003 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
988 1004 assert_equal 2, Issue.find(1).project_id
989 1005 end
990 1006
991 1007 def test_move_one_issue_to_another_project_should_follow_when_needed
992 1008 @request.session[:user_id] = 2
993 1009 post :move, :id => 1, :new_project_id => 2, :follow => '1'
994 1010 assert_redirected_to '/issues/1'
995 1011 end
996 1012
997 1013 def test_bulk_move_to_another_project
998 1014 @request.session[:user_id] = 2
999 1015 post :move, :ids => [1, 2], :new_project_id => 2
1000 1016 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1001 1017 # Issues moved to project 2
1002 1018 assert_equal 2, Issue.find(1).project_id
1003 1019 assert_equal 2, Issue.find(2).project_id
1004 1020 # No tracker change
1005 1021 assert_equal 1, Issue.find(1).tracker_id
1006 1022 assert_equal 2, Issue.find(2).tracker_id
1007 1023 end
1008 1024
1009 1025 def test_bulk_move_to_another_tracker
1010 1026 @request.session[:user_id] = 2
1011 1027 post :move, :ids => [1, 2], :new_tracker_id => 2
1012 1028 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1013 1029 assert_equal 2, Issue.find(1).tracker_id
1014 1030 assert_equal 2, Issue.find(2).tracker_id
1015 1031 end
1016 1032
1017 1033 def test_bulk_copy_to_another_project
1018 1034 @request.session[:user_id] = 2
1019 1035 assert_difference 'Issue.count', 2 do
1020 1036 assert_no_difference 'Project.find(1).issues.count' do
1021 1037 post :move, :ids => [1, 2], :new_project_id => 2, :copy_options => {:copy => '1'}
1022 1038 end
1023 1039 end
1024 1040 assert_redirected_to 'projects/ecookbook/issues'
1025 1041 end
1026 1042
1027 1043 def test_copy_to_another_project_should_follow_when_needed
1028 1044 @request.session[:user_id] = 2
1029 1045 post :move, :ids => [1], :new_project_id => 2, :copy_options => {:copy => '1'}, :follow => '1'
1030 1046 issue = Issue.first(:order => 'id DESC')
1031 1047 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
1032 1048 end
1033 1049
1034 1050 def test_context_menu_one_issue
1035 1051 @request.session[:user_id] = 2
1036 1052 get :context_menu, :ids => [1]
1037 1053 assert_response :success
1038 1054 assert_template 'context_menu'
1039 1055 assert_tag :tag => 'a', :content => 'Edit',
1040 1056 :attributes => { :href => '/issues/1/edit',
1041 1057 :class => 'icon-edit' }
1042 1058 assert_tag :tag => 'a', :content => 'Closed',
1043 1059 :attributes => { :href => '/issues/1/edit?issue%5Bstatus_id%5D=5',
1044 1060 :class => '' }
1045 1061 assert_tag :tag => 'a', :content => 'Immediate',
1046 1062 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;priority_id=8',
1047 1063 :class => '' }
1048 1064 assert_tag :tag => 'a', :content => 'Dave Lopper',
1049 1065 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1',
1050 1066 :class => '' }
1051 1067 assert_tag :tag => 'a', :content => 'Copy',
1052 1068 :attributes => { :href => '/projects/ecookbook/issues/1/copy',
1053 1069 :class => 'icon-copy' }
1054 1070 assert_tag :tag => 'a', :content => 'Move',
1055 1071 :attributes => { :href => '/issues/move?ids%5B%5D=1',
1056 1072 :class => 'icon-move' }
1057 1073 assert_tag :tag => 'a', :content => 'Delete',
1058 1074 :attributes => { :href => '/issues/destroy?ids%5B%5D=1',
1059 1075 :class => 'icon-del' }
1060 1076 end
1061 1077
1062 1078 def test_context_menu_one_issue_by_anonymous
1063 1079 get :context_menu, :ids => [1]
1064 1080 assert_response :success
1065 1081 assert_template 'context_menu'
1066 1082 assert_tag :tag => 'a', :content => 'Delete',
1067 1083 :attributes => { :href => '#',
1068 1084 :class => 'icon-del disabled' }
1069 1085 end
1070 1086
1071 1087 def test_context_menu_multiple_issues_of_same_project
1072 1088 @request.session[:user_id] = 2
1073 1089 get :context_menu, :ids => [1, 2]
1074 1090 assert_response :success
1075 1091 assert_template 'context_menu'
1076 1092 assert_tag :tag => 'a', :content => 'Edit',
1077 1093 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2',
1078 1094 :class => 'icon-edit' }
1079 1095 assert_tag :tag => 'a', :content => 'Immediate',
1080 1096 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2&amp;priority_id=8',
1081 1097 :class => '' }
1082 1098 assert_tag :tag => 'a', :content => 'Dave Lopper',
1083 1099 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1&amp;ids%5B%5D=2',
1084 1100 :class => '' }
1085 1101 assert_tag :tag => 'a', :content => 'Move',
1086 1102 :attributes => { :href => '/issues/move?ids%5B%5D=1&amp;ids%5B%5D=2',
1087 1103 :class => 'icon-move' }
1088 1104 assert_tag :tag => 'a', :content => 'Delete',
1089 1105 :attributes => { :href => '/issues/destroy?ids%5B%5D=1&amp;ids%5B%5D=2',
1090 1106 :class => 'icon-del' }
1091 1107 end
1092 1108
1093 1109 def test_context_menu_multiple_issues_of_different_project
1094 1110 @request.session[:user_id] = 2
1095 1111 get :context_menu, :ids => [1, 2, 4]
1096 1112 assert_response :success
1097 1113 assert_template 'context_menu'
1098 1114 assert_tag :tag => 'a', :content => 'Delete',
1099 1115 :attributes => { :href => '#',
1100 1116 :class => 'icon-del disabled' }
1101 1117 end
1102 1118
1103 1119 def test_destroy_routing
1104 1120 assert_recognizes( #TODO: use DELETE on issue URI (need to change forms)
1105 1121 {:controller => 'issues', :action => 'destroy', :id => '1'},
1106 1122 {:method => :post, :path => '/issues/1/destroy'}
1107 1123 )
1108 1124 end
1109 1125
1110 1126 def test_destroy_issue_with_no_time_entries
1111 1127 assert_nil TimeEntry.find_by_issue_id(2)
1112 1128 @request.session[:user_id] = 2
1113 1129 post :destroy, :id => 2
1114 1130 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1115 1131 assert_nil Issue.find_by_id(2)
1116 1132 end
1117 1133
1118 1134 def test_destroy_issues_with_time_entries
1119 1135 @request.session[:user_id] = 2
1120 1136 post :destroy, :ids => [1, 3]
1121 1137 assert_response :success
1122 1138 assert_template 'destroy'
1123 1139 assert_not_nil assigns(:hours)
1124 1140 assert Issue.find_by_id(1) && Issue.find_by_id(3)
1125 1141 end
1126 1142
1127 1143 def test_destroy_issues_and_destroy_time_entries
1128 1144 @request.session[:user_id] = 2
1129 1145 post :destroy, :ids => [1, 3], :todo => 'destroy'
1130 1146 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1131 1147 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1132 1148 assert_nil TimeEntry.find_by_id([1, 2])
1133 1149 end
1134 1150
1135 1151 def test_destroy_issues_and_assign_time_entries_to_project
1136 1152 @request.session[:user_id] = 2
1137 1153 post :destroy, :ids => [1, 3], :todo => 'nullify'
1138 1154 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1139 1155 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1140 1156 assert_nil TimeEntry.find(1).issue_id
1141 1157 assert_nil TimeEntry.find(2).issue_id
1142 1158 end
1143 1159
1144 1160 def test_destroy_issues_and_reassign_time_entries_to_another_issue
1145 1161 @request.session[:user_id] = 2
1146 1162 post :destroy, :ids => [1, 3], :todo => 'reassign', :reassign_to_id => 2
1147 1163 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1148 1164 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1149 1165 assert_equal 2, TimeEntry.find(1).issue_id
1150 1166 assert_equal 2, TimeEntry.find(2).issue_id
1151 1167 end
1152 1168
1153 1169 def test_default_search_scope
1154 1170 get :index
1155 1171 assert_tag :div, :attributes => {:id => 'quick-search'},
1156 1172 :child => {:tag => 'form',
1157 1173 :child => {:tag => 'input', :attributes => {:name => 'issues', :type => 'hidden', :value => '1'}}}
1158 1174 end
1159 1175 end
General Comments 0
You need to be logged in to leave comments. Login now