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