##// END OF EJS Templates
Cross-project gantt and calendar (#1157)....
Jean-Philippe Lang -
r2086:50794b08a925
parent child
Show More
@@ -1,493 +1,493
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
21 21 before_filter :find_issue, :only => [:show, :edit, :reply, :destroy_attachment]
22 22 before_filter :find_issues, :only => [:bulk_edit, :move, :destroy]
23 before_filter :find_project, :only => [:new, :update_form, :preview, :gantt, :calendar]
24 before_filter :authorize, :except => [:index, :changes, :preview, :update_form, :context_menu]
25 before_filter :find_optional_project, :only => [:index, :changes]
23 before_filter :find_project, :only => [:new, :update_form, :preview]
24 before_filter :authorize, :except => [:index, :changes, :gantt, :calendar, :preview, :update_form, :context_menu]
25 before_filter :find_optional_project, :only => [:index, :changes, :gantt, :calendar]
26 26 accept_key_auth :index, :changes
27 27
28 28 helper :journals
29 29 helper :projects
30 30 include ProjectsHelper
31 31 helper :custom_fields
32 32 include CustomFieldsHelper
33 33 helper :ifpdf
34 34 include IfpdfHelper
35 35 helper :issue_relations
36 36 include IssueRelationsHelper
37 37 helper :watchers
38 38 include WatchersHelper
39 39 helper :attachments
40 40 include AttachmentsHelper
41 41 helper :queries
42 42 helper :sort
43 43 include SortHelper
44 44 include IssuesHelper
45 45 helper :timelog
46 46
47 47 def index
48 48 sort_init "#{Issue.table_name}.id", "desc"
49 49 sort_update
50 50 retrieve_query
51 51 if @query.valid?
52 52 limit = per_page_option
53 53 respond_to do |format|
54 54 format.html { }
55 55 format.atom { }
56 56 format.csv { limit = Setting.issues_export_limit.to_i }
57 57 format.pdf { limit = Setting.issues_export_limit.to_i }
58 58 end
59 59 @issue_count = Issue.count(:include => [:status, :project], :conditions => @query.statement)
60 60 @issue_pages = Paginator.new self, @issue_count, limit, params['page']
61 61 @issues = Issue.find :all, :order => sort_clause,
62 62 :include => [ :assigned_to, :status, :tracker, :project, :priority, :category, :fixed_version ],
63 63 :conditions => @query.statement,
64 64 :limit => limit,
65 65 :offset => @issue_pages.current.offset
66 66 respond_to do |format|
67 67 format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
68 68 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
69 69 format.csv { send_data(issues_to_csv(@issues, @project).read, :type => 'text/csv; header=present', :filename => 'export.csv') }
70 70 format.pdf { send_data(render(:template => 'issues/index.rfpdf', :layout => false), :type => 'application/pdf', :filename => 'export.pdf') }
71 71 end
72 72 else
73 73 # Send html if the query is not valid
74 74 render(:template => 'issues/index.rhtml', :layout => !request.xhr?)
75 75 end
76 76 rescue ActiveRecord::RecordNotFound
77 77 render_404
78 78 end
79 79
80 80 def changes
81 81 sort_init "#{Issue.table_name}.id", "desc"
82 82 sort_update
83 83 retrieve_query
84 84 if @query.valid?
85 85 @journals = Journal.find :all, :include => [ :details, :user, {:issue => [:project, :author, :tracker, :status]} ],
86 86 :conditions => @query.statement,
87 87 :limit => 25,
88 88 :order => "#{Journal.table_name}.created_on DESC"
89 89 end
90 90 @title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
91 91 render :layout => false, :content_type => 'application/atom+xml'
92 92 rescue ActiveRecord::RecordNotFound
93 93 render_404
94 94 end
95 95
96 96 def show
97 97 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
98 98 @journals.each_with_index {|j,i| j.indice = i+1}
99 99 @journals.reverse! if User.current.wants_comments_in_reverse_order?
100 100 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
101 101 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
102 102 @priorities = Enumeration::get_values('IPRI')
103 103 @time_entry = TimeEntry.new
104 104 respond_to do |format|
105 105 format.html { render :template => 'issues/show.rhtml' }
106 106 format.atom { render :action => 'changes', :layout => false, :content_type => 'application/atom+xml' }
107 107 format.pdf { send_data(render(:template => 'issues/show.rfpdf', :layout => false), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
108 108 end
109 109 end
110 110
111 111 # Add a new issue
112 112 # The new issue will be created from an existing one if copy_from parameter is given
113 113 def new
114 114 @issue = Issue.new
115 115 @issue.copy_from(params[:copy_from]) if params[:copy_from]
116 116 @issue.project = @project
117 117 # Tracker must be set before custom field values
118 118 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
119 119 if @issue.tracker.nil?
120 120 flash.now[:error] = 'No tracker is associated to this project. Please check the Project settings.'
121 121 render :nothing => true, :layout => true
122 122 return
123 123 end
124 124 @issue.attributes = params[:issue]
125 125 @issue.author = User.current
126 126
127 127 default_status = IssueStatus.default
128 128 unless default_status
129 129 flash.now[:error] = 'No default issue status is defined. Please check your configuration (Go to "Administration -> Issue statuses").'
130 130 render :nothing => true, :layout => true
131 131 return
132 132 end
133 133 @issue.status = default_status
134 134 @allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(User.current.role_for_project(@project), @issue.tracker)).uniq
135 135
136 136 if request.get? || request.xhr?
137 137 @issue.start_date ||= Date.today
138 138 else
139 139 requested_status = IssueStatus.find_by_id(params[:issue][:status_id])
140 140 # Check that the user is allowed to apply the requested status
141 141 @issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status
142 142 if @issue.save
143 143 attach_files(@issue, params[:attachments])
144 144 flash[:notice] = l(:notice_successful_create)
145 145 Mailer.deliver_issue_add(@issue) if Setting.notified_events.include?('issue_added')
146 146 redirect_to :controller => 'issues', :action => 'show', :id => @issue
147 147 return
148 148 end
149 149 end
150 150 @priorities = Enumeration::get_values('IPRI')
151 151 render :layout => !request.xhr?
152 152 end
153 153
154 154 # Attributes that can be updated on workflow transition (without :edit permission)
155 155 # TODO: make it configurable (at least per role)
156 156 UPDATABLE_ATTRS_ON_TRANSITION = %w(status_id assigned_to_id fixed_version_id done_ratio) unless const_defined?(:UPDATABLE_ATTRS_ON_TRANSITION)
157 157
158 158 def edit
159 159 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
160 160 @priorities = Enumeration::get_values('IPRI')
161 161 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
162 162 @time_entry = TimeEntry.new
163 163
164 164 @notes = params[:notes]
165 165 journal = @issue.init_journal(User.current, @notes)
166 166 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
167 167 if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue]
168 168 attrs = params[:issue].dup
169 169 attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed
170 170 attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s}
171 171 @issue.attributes = attrs
172 172 end
173 173
174 174 if request.post?
175 175 @time_entry = TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
176 176 @time_entry.attributes = params[:time_entry]
177 177 attachments = attach_files(@issue, params[:attachments])
178 178 attachments.each {|a| journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
179 179
180 180 call_hook(:controller_issues_edit_before_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
181 181
182 182 if (@time_entry.hours.nil? || @time_entry.valid?) && @issue.save
183 183 # Log spend time
184 184 if current_role.allowed_to?(:log_time)
185 185 @time_entry.save
186 186 end
187 187 if !journal.new_record?
188 188 # Only send notification if something was actually changed
189 189 flash[:notice] = l(:notice_successful_update)
190 190 Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
191 191 end
192 192 redirect_to(params[:back_to] || {:action => 'show', :id => @issue})
193 193 end
194 194 end
195 195 rescue ActiveRecord::StaleObjectError
196 196 # Optimistic locking exception
197 197 flash.now[:error] = l(:notice_locking_conflict)
198 198 end
199 199
200 200 def reply
201 201 journal = Journal.find(params[:journal_id]) if params[:journal_id]
202 202 if journal
203 203 user = journal.user
204 204 text = journal.notes
205 205 else
206 206 user = @issue.author
207 207 text = @issue.description
208 208 end
209 209 content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
210 210 content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
211 211 render(:update) { |page|
212 212 page.<< "$('notes').value = \"#{content}\";"
213 213 page.show 'update'
214 214 page << "Form.Element.focus('notes');"
215 215 page << "Element.scrollTo('update');"
216 216 page << "$('notes').scrollTop = $('notes').scrollHeight - $('notes').clientHeight;"
217 217 }
218 218 end
219 219
220 220 # Bulk edit a set of issues
221 221 def bulk_edit
222 222 if request.post?
223 223 status = params[:status_id].blank? ? nil : IssueStatus.find_by_id(params[:status_id])
224 224 priority = params[:priority_id].blank? ? nil : Enumeration.find_by_id(params[:priority_id])
225 225 assigned_to = (params[:assigned_to_id].blank? || params[:assigned_to_id] == 'none') ? nil : User.find_by_id(params[:assigned_to_id])
226 226 category = (params[:category_id].blank? || params[:category_id] == 'none') ? nil : @project.issue_categories.find_by_id(params[:category_id])
227 227 fixed_version = (params[:fixed_version_id].blank? || params[:fixed_version_id] == 'none') ? nil : @project.versions.find_by_id(params[:fixed_version_id])
228 228
229 229 unsaved_issue_ids = []
230 230 @issues.each do |issue|
231 231 journal = issue.init_journal(User.current, params[:notes])
232 232 issue.priority = priority if priority
233 233 issue.assigned_to = assigned_to if assigned_to || params[:assigned_to_id] == 'none'
234 234 issue.category = category if category || params[:category_id] == 'none'
235 235 issue.fixed_version = fixed_version if fixed_version || params[:fixed_version_id] == 'none'
236 236 issue.start_date = params[:start_date] unless params[:start_date].blank?
237 237 issue.due_date = params[:due_date] unless params[:due_date].blank?
238 238 issue.done_ratio = params[:done_ratio] unless params[:done_ratio].blank?
239 239 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
240 240 # Don't save any change to the issue if the user is not authorized to apply the requested status
241 241 if (status.nil? || (issue.status.new_status_allowed_to?(status, current_role, issue.tracker) && issue.status = status)) && issue.save
242 242 # Send notification for each issue (if changed)
243 243 Mailer.deliver_issue_edit(journal) if journal.details.any? && Setting.notified_events.include?('issue_updated')
244 244 else
245 245 # Keep unsaved issue ids to display them in flash error
246 246 unsaved_issue_ids << issue.id
247 247 end
248 248 end
249 249 if unsaved_issue_ids.empty?
250 250 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
251 251 else
252 252 flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #'))
253 253 end
254 254 redirect_to(params[:back_to] || {:controller => 'issues', :action => 'index', :project_id => @project})
255 255 return
256 256 end
257 257 # Find potential statuses the user could be allowed to switch issues to
258 258 @available_statuses = Workflow.find(:all, :include => :new_status,
259 259 :conditions => {:role_id => current_role.id}).collect(&:new_status).compact.uniq.sort
260 260 end
261 261
262 262 def move
263 263 @allowed_projects = []
264 264 # find projects to which the user is allowed to move the issue
265 265 if User.current.admin?
266 266 # admin is allowed to move issues to any active (visible) project
267 267 @allowed_projects = Project.find(:all, :conditions => Project.visible_by(User.current), :order => 'name')
268 268 else
269 269 User.current.memberships.each {|m| @allowed_projects << m.project if m.role.allowed_to?(:move_issues)}
270 270 end
271 271 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id]
272 272 @target_project ||= @project
273 273 @trackers = @target_project.trackers
274 274 if request.post?
275 275 new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id])
276 276 unsaved_issue_ids = []
277 277 @issues.each do |issue|
278 278 issue.init_journal(User.current)
279 279 unsaved_issue_ids << issue.id unless issue.move_to(@target_project, new_tracker)
280 280 end
281 281 if unsaved_issue_ids.empty?
282 282 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
283 283 else
284 284 flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #'))
285 285 end
286 286 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
287 287 return
288 288 end
289 289 render :layout => false if request.xhr?
290 290 end
291 291
292 292 def destroy
293 293 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
294 294 if @hours > 0
295 295 case params[:todo]
296 296 when 'destroy'
297 297 # nothing to do
298 298 when 'nullify'
299 299 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
300 300 when 'reassign'
301 301 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
302 302 if reassign_to.nil?
303 303 flash.now[:error] = l(:error_issue_not_found_in_project)
304 304 return
305 305 else
306 306 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
307 307 end
308 308 else
309 309 # display the destroy form
310 310 return
311 311 end
312 312 end
313 313 @issues.each(&:destroy)
314 314 redirect_to :action => 'index', :project_id => @project
315 315 end
316 316
317 317 def destroy_attachment
318 318 a = @issue.attachments.find(params[:attachment_id])
319 319 a.destroy
320 320 journal = @issue.init_journal(User.current)
321 321 journal.details << JournalDetail.new(:property => 'attachment',
322 322 :prop_key => a.id,
323 323 :old_value => a.filename)
324 324 journal.save
325 325 redirect_to :action => 'show', :id => @issue
326 326 end
327 327
328 328 def gantt
329 329 @gantt = Redmine::Helpers::Gantt.new(params)
330 330 retrieve_query
331 331 if @query.valid?
332 332 events = []
333 333 # Issues that have start and due dates
334 334 events += Issue.find(:all,
335 335 :order => "start_date, due_date",
336 336 :include => [:tracker, :status, :assigned_to, :priority, :project],
337 337 :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]
338 338 )
339 339 # Issues that don't have a due date but that are assigned to a version with a date
340 340 events += Issue.find(:all,
341 341 :order => "start_date, effective_date",
342 342 :include => [:tracker, :status, :assigned_to, :priority, :project, :fixed_version],
343 343 :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]
344 344 )
345 345 # Versions
346 346 events += Version.find(:all, :include => :project,
347 347 :conditions => ["(#{@query.project_statement}) AND effective_date BETWEEN ? AND ?", @gantt.date_from, @gantt.date_to])
348 348
349 349 @gantt.events = events
350 350 end
351 351
352 352 respond_to do |format|
353 353 format.html { render :template => "issues/gantt.rhtml", :layout => !request.xhr? }
354 354 format.png { send_data(@gantt.to_image, :disposition => 'inline', :type => 'image/png', :filename => "#{@project.identifier}-gantt.png") } if @gantt.respond_to?('to_image')
355 format.pdf { send_data(render(:template => "issues/gantt.rfpdf", :layout => false), :type => 'application/pdf', :filename => "#{@project.identifier}-gantt.pdf") }
355 format.pdf { send_data(render(:template => "issues/gantt.rfpdf", :layout => false), :type => 'application/pdf', :filename => "#{@project.nil? ? '' : "#{@project.identifier}-" }gantt.pdf") }
356 356 end
357 357 end
358 358
359 359 def calendar
360 360 if params[:year] and params[:year].to_i > 1900
361 361 @year = params[:year].to_i
362 362 if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
363 363 @month = params[:month].to_i
364 364 end
365 365 end
366 366 @year ||= Date.today.year
367 367 @month ||= Date.today.month
368 368
369 369 @calendar = Redmine::Helpers::Calendar.new(Date.civil(@year, @month, 1), current_language, :month)
370 370 retrieve_query
371 371 if @query.valid?
372 372 events = []
373 373 events += Issue.find(:all,
374 374 :include => [:tracker, :status, :assigned_to, :priority, :project],
375 375 :conditions => ["(#{@query.statement}) AND ((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?))", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt]
376 376 )
377 377 events += Version.find(:all, :include => :project,
378 378 :conditions => ["(#{@query.project_statement}) AND effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt])
379 379
380 380 @calendar.events = events
381 381 end
382 382
383 383 render :layout => false if request.xhr?
384 384 end
385 385
386 386 def context_menu
387 387 @issues = Issue.find_all_by_id(params[:ids], :include => :project)
388 388 if (@issues.size == 1)
389 389 @issue = @issues.first
390 390 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
391 391 end
392 392 projects = @issues.collect(&:project).compact.uniq
393 393 @project = projects.first if projects.size == 1
394 394
395 395 @can = {:edit => (@project && User.current.allowed_to?(:edit_issues, @project)),
396 396 :log_time => (@project && User.current.allowed_to?(:log_time, @project)),
397 397 :update => (@project && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && @allowed_statuses && !@allowed_statuses.empty?))),
398 398 :move => (@project && User.current.allowed_to?(:move_issues, @project)),
399 399 :copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
400 400 :delete => (@project && User.current.allowed_to?(:delete_issues, @project))
401 401 }
402 402 if @project
403 403 @assignables = @project.assignable_users
404 404 @assignables << @issue.assigned_to if @issue && @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
405 405 end
406 406
407 407 @priorities = Enumeration.get_values('IPRI').reverse
408 408 @statuses = IssueStatus.find(:all, :order => 'position')
409 409 @back = request.env['HTTP_REFERER']
410 410
411 411 render :layout => false
412 412 end
413 413
414 414 def update_form
415 415 @issue = Issue.new(params[:issue])
416 416 render :action => :new, :layout => false
417 417 end
418 418
419 419 def preview
420 420 @issue = @project.issues.find_by_id(params[:id]) unless params[:id].blank?
421 421 @attachements = @issue.attachments if @issue
422 422 @text = params[:notes] || (params[:issue] ? params[:issue][:description] : nil)
423 423 render :partial => 'common/preview'
424 424 end
425 425
426 426 private
427 427 def find_issue
428 428 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
429 429 @project = @issue.project
430 430 rescue ActiveRecord::RecordNotFound
431 431 render_404
432 432 end
433 433
434 434 # Filter for bulk operations
435 435 def find_issues
436 436 @issues = Issue.find_all_by_id(params[:id] || params[:ids])
437 437 raise ActiveRecord::RecordNotFound if @issues.empty?
438 438 projects = @issues.collect(&:project).compact.uniq
439 439 if projects.size == 1
440 440 @project = projects.first
441 441 else
442 442 # TODO: let users bulk edit/move/destroy issues from different projects
443 443 render_error 'Can not bulk edit/move/destroy issues from different projects' and return false
444 444 end
445 445 rescue ActiveRecord::RecordNotFound
446 446 render_404
447 447 end
448 448
449 449 def find_project
450 450 @project = Project.find(params[:project_id])
451 451 rescue ActiveRecord::RecordNotFound
452 452 render_404
453 453 end
454 454
455 455 def find_optional_project
456 return true unless params[:project_id]
457 @project = Project.find(params[:project_id])
458 authorize
456 @project = Project.find(params[:project_id]) unless params[:project_id].blank?
457 allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
458 allowed ? true : deny_access
459 459 rescue ActiveRecord::RecordNotFound
460 460 render_404
461 461 end
462 462
463 463 # Retrieve query from session or build a new query
464 464 def retrieve_query
465 465 if !params[:query_id].blank?
466 466 cond = "project_id IS NULL"
467 467 cond << " OR project_id = #{@project.id}" if @project
468 468 @query = Query.find(params[:query_id], :conditions => cond)
469 469 @query.project = @project
470 470 session[:query] = {:id => @query.id, :project_id => @query.project_id}
471 471 else
472 472 if params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
473 473 # Give it a name, required to be valid
474 474 @query = Query.new(:name => "_")
475 475 @query.project = @project
476 476 if params[:fields] and params[:fields].is_a? Array
477 477 params[:fields].each do |field|
478 478 @query.add_filter(field, params[:operators][field], params[:values][field])
479 479 end
480 480 else
481 481 @query.available_filters.keys.each do |field|
482 482 @query.add_short_filter(field, params[field]) if params[field]
483 483 end
484 484 end
485 485 session[:query] = {:project_id => @query.project_id, :filters => @query.filters}
486 486 else
487 487 @query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
488 488 @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters])
489 489 @query.project = @project
490 490 end
491 491 end
492 492 end
493 493 end
@@ -1,24 +1,23
1 1 <h3><%= l(:label_issue_plural) %></h3>
2 2 <%= link_to l(:label_issue_view_all), { :controller => 'issues', :action => 'index', :project_id => @project, :set_filter => 1 } %><br />
3 3 <% if @project %>
4 4 <%= link_to l(:field_summary), :controller => 'reports', :action => 'issue_report', :id => @project %><br />
5 5 <%= link_to l(:label_change_log), :controller => 'projects', :action => 'changelog', :id => @project %>
6 <% end %>
6 7
7 8 <% planning_links = []
8 planning_links << link_to_if_authorized(l(:label_calendar), :action => 'calendar', :project_id => @project)
9 planning_links << link_to_if_authorized(l(:label_gantt), :action => 'gantt', :project_id => @project)
10 planning_links.compact!
11 unless planning_links.empty? %>
9 planning_links << link_to(l(:label_calendar), :action => 'calendar', :project_id => @project) if User.current.allowed_to?(:view_calendar, @project, :global => true)
10 planning_links << link_to(l(:label_gantt), :action => 'gantt', :project_id => @project) if User.current.allowed_to?(:view_gantt, @project, :global => true)
11 %>
12 <% unless planning_links.empty? %>
12 13 <h3><%= l(:label_planning) %></h3>
13 14 <p><%= planning_links.join(' | ') %></p>
14 15 <% end %>
15 16
16 <% end %>
17
18 17 <% unless sidebar_queries.empty? -%>
19 18 <h3><%= l(:label_query_plural) %></h3>
20 19
21 20 <% sidebar_queries.each do |query| -%>
22 21 <%= link_to query.name, :controller => 'issues', :action => 'index', :project_id => @project, :query_id => query %><br />
23 22 <% end -%>
24 23 <% end -%>
@@ -1,188 +1,188
1 1 <%
2 2 pdf=IfpdfHelper::IFPDF.new(current_language)
3 pdf.SetTitle("#{@project.name} - #{l(:label_gantt)}")
3 pdf.SetTitle("#{l(:label_gantt)} #{@project}")
4 4 pdf.AliasNbPages
5 5 pdf.footer_date = format_date(Date.today)
6 6 pdf.AddPage("L")
7 7 pdf.SetFontStyle('B',12)
8 8 pdf.SetX(15)
9 pdf.Cell(70, 20, @project.name)
9 pdf.Cell(70, 20, @project.to_s)
10 10 pdf.Ln
11 11 pdf.SetFontStyle('B',9)
12 12
13 13 subject_width = 70
14 14 header_heigth = 5
15 15
16 16 headers_heigth = header_heigth
17 17 show_weeks = false
18 18 show_days = false
19 19
20 20 if @gantt.months < 7
21 21 show_weeks = true
22 22 headers_heigth = 2*header_heigth
23 23 if @gantt.months < 3
24 24 show_days = true
25 25 headers_heigth = 3*header_heigth
26 26 end
27 27 end
28 28
29 29 g_width = 210
30 30 zoom = (g_width) / (@gantt.date_to - @gantt.date_from + 1)
31 31 g_height = 120
32 32 t_height = g_height + headers_heigth
33 33
34 34 y_start = pdf.GetY
35 35
36 36
37 37 #
38 38 # Months headers
39 39 #
40 40 month_f = @gantt.date_from
41 41 left = subject_width
42 42 height = header_heigth
43 43 @gantt.months.times do
44 44 width = ((month_f >> 1) - month_f) * zoom
45 45 pdf.SetY(y_start)
46 46 pdf.SetX(left)
47 47 pdf.Cell(width, height, "#{month_f.year}-#{month_f.month}", "LTR", 0, "C")
48 48 left = left + width
49 49 month_f = month_f >> 1
50 50 end
51 51
52 52 #
53 53 # Weeks headers
54 54 #
55 55 if show_weeks
56 56 left = subject_width
57 57 height = header_heigth
58 58 if @gantt.date_from.cwday == 1
59 59 # @gantt.date_from is monday
60 60 week_f = @gantt.date_from
61 61 else
62 62 # find next monday after @gantt.date_from
63 63 week_f = @gantt.date_from + (7 - @gantt.date_from.cwday + 1)
64 64 width = (7 - @gantt.date_from.cwday + 1) * zoom-1
65 65 pdf.SetY(y_start + header_heigth)
66 66 pdf.SetX(left)
67 67 pdf.Cell(width + 1, height, "", "LTR")
68 68 left = left + width+1
69 69 end
70 70 while week_f <= @gantt.date_to
71 71 width = (week_f + 6 <= @gantt.date_to) ? 7 * zoom : (@gantt.date_to - week_f + 1) * zoom
72 72 pdf.SetY(y_start + header_heigth)
73 73 pdf.SetX(left)
74 74 pdf.Cell(width, height, (width >= 5 ? week_f.cweek.to_s : ""), "LTR", 0, "C")
75 75 left = left + width
76 76 week_f = week_f+7
77 77 end
78 78 end
79 79
80 80 #
81 81 # Days headers
82 82 #
83 83 if show_days
84 84 left = subject_width
85 85 height = header_heigth
86 86 wday = @gantt.date_from.cwday
87 87 pdf.SetFontStyle('B',7)
88 88 (@gantt.date_to - @gantt.date_from + 1).to_i.times do
89 89 width = zoom
90 90 pdf.SetY(y_start + 2 * header_heigth)
91 91 pdf.SetX(left)
92 92 pdf.Cell(width, height, day_name(wday).first, "LTR", 0, "C")
93 93 left = left + width
94 94 wday = wday + 1
95 95 wday = 1 if wday > 7
96 96 end
97 97 end
98 98
99 99 pdf.SetY(y_start)
100 100 pdf.SetX(15)
101 101 pdf.Cell(subject_width+g_width-15, headers_heigth, "", 1)
102 102
103 103
104 104 #
105 105 # Tasks
106 106 #
107 107 top = headers_heigth + y_start
108 108 pdf.SetFontStyle('B',7)
109 109 @gantt.events.each do |i|
110 110 pdf.SetY(top)
111 111 pdf.SetX(15)
112 112
113 113 if i.is_a? Issue
114 114 pdf.Cell(subject_width-15, 5, "#{i.tracker.name} #{i.id}: #{i.subject}".sub(/^(.{30}[^\s]*\s).*$/, '\1 (...)'), "LR")
115 115 else
116 116 pdf.Cell(subject_width-15, 5, "#{l(:label_version)}: #{i.name}", "LR")
117 117 end
118 118
119 119 pdf.SetY(top)
120 120 pdf.SetX(subject_width)
121 121 pdf.Cell(g_width, 5, "", "LR")
122 122
123 123 pdf.SetY(top+1.5)
124 124
125 125 if i.is_a? Issue
126 126 i_start_date = (i.start_date >= @gantt.date_from ? i.start_date : @gantt.date_from )
127 127 i_end_date = (i.due_before <= @gantt.date_to ? i.due_before : @gantt.date_to )
128 128
129 129 i_done_date = i.start_date + ((i.due_before - i.start_date+1)*i.done_ratio/100).floor
130 130 i_done_date = (i_done_date <= @gantt.date_from ? @gantt.date_from : i_done_date )
131 131 i_done_date = (i_done_date >= @gantt.date_to ? @gantt.date_to : i_done_date )
132 132
133 133 i_late_date = [i_end_date, Date.today].min if i_start_date < Date.today
134 134
135 135 i_left = ((i_start_date - @gantt.date_from)*zoom)
136 136 i_width = ((i_end_date - i_start_date + 1)*zoom)
137 137 d_width = ((i_done_date - i_start_date)*zoom)
138 138 l_width = ((i_late_date - i_start_date+1)*zoom) if i_late_date
139 139 l_width ||= 0
140 140
141 141 pdf.SetX(subject_width + i_left)
142 142 pdf.SetFillColor(200,200,200)
143 143 pdf.Cell(i_width, 2, "", 0, 0, "", 1)
144 144
145 145 if l_width > 0
146 146 pdf.SetY(top+1.5)
147 147 pdf.SetX(subject_width + i_left)
148 148 pdf.SetFillColor(255,100,100)
149 149 pdf.Cell(l_width, 2, "", 0, 0, "", 1)
150 150 end
151 151 if d_width > 0
152 152 pdf.SetY(top+1.5)
153 153 pdf.SetX(subject_width + i_left)
154 154 pdf.SetFillColor(100,100,255)
155 155 pdf.Cell(d_width, 2, "", 0, 0, "", 1)
156 156 end
157 157
158 158 pdf.SetY(top+1.5)
159 159 pdf.SetX(subject_width + i_left + i_width)
160 160 pdf.Cell(30, 2, "#{i.status.name} #{i.done_ratio}%")
161 161 else
162 162 i_left = ((i.start_date - @gantt.date_from)*zoom)
163 163
164 164 pdf.SetX(subject_width + i_left)
165 165 pdf.SetFillColor(50,200,50)
166 166 pdf.Cell(2, 2, "", 0, 0, "", 1)
167 167
168 168 pdf.SetY(top+1.5)
169 169 pdf.SetX(subject_width + i_left + 3)
170 170 pdf.Cell(30, 2, "#{i.name}")
171 171 end
172 172
173 173
174 174 top = top + 5
175 175 pdf.SetDrawColor(200, 200, 200)
176 176 pdf.Line(15, top, subject_width+g_width, top)
177 177 if pdf.GetY() > 180
178 178 pdf.AddPage("L")
179 179 top = 20
180 180 pdf.Line(15, top, subject_width+g_width, top)
181 181 end
182 182 pdf.SetDrawColor(0, 0, 0)
183 183 end
184 184
185 185 pdf.Line(15, top, subject_width+g_width, top)
186 186
187 187 %>
188 188 <%= pdf.Output %> No newline at end of file
@@ -1,705 +1,729
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 < Test::Unit::TestCase
25 25 fixtures :projects,
26 26 :users,
27 27 :roles,
28 28 :members,
29 29 :issues,
30 30 :issue_statuses,
31 31 :versions,
32 32 :trackers,
33 33 :projects_trackers,
34 34 :issue_categories,
35 35 :enabled_modules,
36 36 :enumerations,
37 37 :attachments,
38 38 :workflows,
39 39 :custom_fields,
40 40 :custom_values,
41 41 :custom_fields_trackers,
42 42 :time_entries,
43 43 :journals,
44 44 :journal_details
45 45
46 46 def setup
47 47 @controller = IssuesController.new
48 48 @request = ActionController::TestRequest.new
49 49 @response = ActionController::TestResponse.new
50 50 User.current = nil
51 51 end
52 52
53 53 def test_index
54 54 get :index
55 55 assert_response :success
56 56 assert_template 'index.rhtml'
57 57 assert_not_nil assigns(:issues)
58 58 assert_nil assigns(:project)
59 59 assert_tag :tag => 'a', :content => /Can't print recipes/
60 60 assert_tag :tag => 'a', :content => /Subproject issue/
61 61 # private projects hidden
62 62 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
63 63 assert_no_tag :tag => 'a', :content => /Issue on project 2/
64 64 end
65 65
66 66 def test_index_should_not_list_issues_when_module_disabled
67 67 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
68 68 get :index
69 69 assert_response :success
70 70 assert_template 'index.rhtml'
71 71 assert_not_nil assigns(:issues)
72 72 assert_nil assigns(:project)
73 73 assert_no_tag :tag => 'a', :content => /Can't print recipes/
74 74 assert_tag :tag => 'a', :content => /Subproject issue/
75 75 end
76 76
77 77 def test_index_with_project
78 78 Setting.display_subprojects_issues = 0
79 79 get :index, :project_id => 1
80 80 assert_response :success
81 81 assert_template 'index.rhtml'
82 82 assert_not_nil assigns(:issues)
83 83 assert_tag :tag => 'a', :content => /Can't print recipes/
84 84 assert_no_tag :tag => 'a', :content => /Subproject issue/
85 85 end
86 86
87 87 def test_index_with_project_and_subprojects
88 88 Setting.display_subprojects_issues = 1
89 89 get :index, :project_id => 1
90 90 assert_response :success
91 91 assert_template 'index.rhtml'
92 92 assert_not_nil assigns(:issues)
93 93 assert_tag :tag => 'a', :content => /Can't print recipes/
94 94 assert_tag :tag => 'a', :content => /Subproject issue/
95 95 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
96 96 end
97 97
98 98 def test_index_with_project_and_subprojects_should_show_private_subprojects
99 99 @request.session[:user_id] = 2
100 100 Setting.display_subprojects_issues = 1
101 101 get :index, :project_id => 1
102 102 assert_response :success
103 103 assert_template 'index.rhtml'
104 104 assert_not_nil assigns(:issues)
105 105 assert_tag :tag => 'a', :content => /Can't print recipes/
106 106 assert_tag :tag => 'a', :content => /Subproject issue/
107 107 assert_tag :tag => 'a', :content => /Issue of a private subproject/
108 108 end
109 109
110 110 def test_index_with_project_and_filter
111 111 get :index, :project_id => 1, :set_filter => 1
112 112 assert_response :success
113 113 assert_template 'index.rhtml'
114 114 assert_not_nil assigns(:issues)
115 115 end
116 116
117 117 def test_index_csv_with_project
118 118 get :index, :format => 'csv'
119 119 assert_response :success
120 120 assert_not_nil assigns(:issues)
121 121 assert_equal 'text/csv', @response.content_type
122 122
123 123 get :index, :project_id => 1, :format => 'csv'
124 124 assert_response :success
125 125 assert_not_nil assigns(:issues)
126 126 assert_equal 'text/csv', @response.content_type
127 127 end
128 128
129 129 def test_index_pdf
130 130 get :index, :format => 'pdf'
131 131 assert_response :success
132 132 assert_not_nil assigns(:issues)
133 133 assert_equal 'application/pdf', @response.content_type
134 134
135 135 get :index, :project_id => 1, :format => 'pdf'
136 136 assert_response :success
137 137 assert_not_nil assigns(:issues)
138 138 assert_equal 'application/pdf', @response.content_type
139 139 end
140 140
141 141 def test_gantt
142 142 get :gantt, :project_id => 1
143 143 assert_response :success
144 144 assert_template 'gantt.rhtml'
145 145 assert_not_nil assigns(:gantt)
146 146 events = assigns(:gantt).events
147 147 assert_not_nil events
148 148 # Issue with start and due dates
149 149 i = Issue.find(1)
150 150 assert_not_nil i.due_date
151 151 assert events.include?(Issue.find(1))
152 152 # Issue with without due date but targeted to a version with date
153 153 i = Issue.find(2)
154 154 assert_nil i.due_date
155 155 assert events.include?(i)
156 156 end
157 157
158 def test_cross_project_gantt
159 get :gantt
160 assert_response :success
161 assert_template 'gantt.rhtml'
162 assert_not_nil assigns(:gantt)
163 events = assigns(:gantt).events
164 assert_not_nil events
165 end
166
158 167 def test_gantt_export_to_pdf
159 168 get :gantt, :project_id => 1, :format => 'pdf'
160 169 assert_response :success
161 170 assert_template 'gantt.rfpdf'
162 171 assert_equal 'application/pdf', @response.content_type
163 172 assert_not_nil assigns(:gantt)
164 173 end
165 174
175 def test_cross_project_gantt_export_to_pdf
176 get :gantt, :format => 'pdf'
177 assert_response :success
178 assert_template 'gantt.rfpdf'
179 assert_equal 'application/pdf', @response.content_type
180 assert_not_nil assigns(:gantt)
181 end
182
166 183 if Object.const_defined?(:Magick)
167 184 def test_gantt_image
168 185 get :gantt, :project_id => 1, :format => 'png'
169 186 assert_response :success
170 187 assert_equal 'image/png', @response.content_type
171 188 end
172 189 else
173 190 puts "RMagick not installed. Skipping tests !!!"
174 191 end
175 192
176 193 def test_calendar
177 194 get :calendar, :project_id => 1
178 195 assert_response :success
179 196 assert_template 'calendar'
180 197 assert_not_nil assigns(:calendar)
181 198 end
182 199
200 def test_cross_project_calendar
201 get :calendar
202 assert_response :success
203 assert_template 'calendar'
204 assert_not_nil assigns(:calendar)
205 end
206
183 207 def test_changes
184 208 get :changes, :project_id => 1
185 209 assert_response :success
186 210 assert_not_nil assigns(:journals)
187 211 assert_equal 'application/atom+xml', @response.content_type
188 212 end
189 213
190 214 def test_show_by_anonymous
191 215 get :show, :id => 1
192 216 assert_response :success
193 217 assert_template 'show.rhtml'
194 218 assert_not_nil assigns(:issue)
195 219 assert_equal Issue.find(1), assigns(:issue)
196 220
197 221 # anonymous role is allowed to add a note
198 222 assert_tag :tag => 'form',
199 223 :descendant => { :tag => 'fieldset',
200 224 :child => { :tag => 'legend',
201 225 :content => /Notes/ } }
202 226 end
203 227
204 228 def test_show_by_manager
205 229 @request.session[:user_id] = 2
206 230 get :show, :id => 1
207 231 assert_response :success
208 232
209 233 assert_tag :tag => 'form',
210 234 :descendant => { :tag => 'fieldset',
211 235 :child => { :tag => 'legend',
212 236 :content => /Change properties/ } },
213 237 :descendant => { :tag => 'fieldset',
214 238 :child => { :tag => 'legend',
215 239 :content => /Log time/ } },
216 240 :descendant => { :tag => 'fieldset',
217 241 :child => { :tag => 'legend',
218 242 :content => /Notes/ } }
219 243 end
220 244
221 245 def test_get_new
222 246 @request.session[:user_id] = 2
223 247 get :new, :project_id => 1, :tracker_id => 1
224 248 assert_response :success
225 249 assert_template 'new'
226 250
227 251 assert_tag :tag => 'input', :attributes => { :name => 'issue[custom_field_values][2]',
228 252 :value => 'Default string' }
229 253 end
230 254
231 255 def test_get_new_without_tracker_id
232 256 @request.session[:user_id] = 2
233 257 get :new, :project_id => 1
234 258 assert_response :success
235 259 assert_template 'new'
236 260
237 261 issue = assigns(:issue)
238 262 assert_not_nil issue
239 263 assert_equal Project.find(1).trackers.first, issue.tracker
240 264 end
241 265
242 266 def test_update_new_form
243 267 @request.session[:user_id] = 2
244 268 xhr :post, :new, :project_id => 1,
245 269 :issue => {:tracker_id => 2,
246 270 :subject => 'This is the test_new issue',
247 271 :description => 'This is the description',
248 272 :priority_id => 5}
249 273 assert_response :success
250 274 assert_template 'new'
251 275 end
252 276
253 277 def test_post_new
254 278 @request.session[:user_id] = 2
255 279 post :new, :project_id => 1,
256 280 :issue => {:tracker_id => 3,
257 281 :subject => 'This is the test_new issue',
258 282 :description => 'This is the description',
259 283 :priority_id => 5,
260 284 :estimated_hours => '',
261 285 :custom_field_values => {'2' => 'Value for field 2'}}
262 286 assert_redirected_to 'issues/show'
263 287
264 288 issue = Issue.find_by_subject('This is the test_new issue')
265 289 assert_not_nil issue
266 290 assert_equal 2, issue.author_id
267 291 assert_equal 3, issue.tracker_id
268 292 assert_nil issue.estimated_hours
269 293 v = issue.custom_values.find(:first, :conditions => {:custom_field_id => 2})
270 294 assert_not_nil v
271 295 assert_equal 'Value for field 2', v.value
272 296 end
273 297
274 298 def test_post_new_without_custom_fields_param
275 299 @request.session[:user_id] = 2
276 300 post :new, :project_id => 1,
277 301 :issue => {:tracker_id => 1,
278 302 :subject => 'This is the test_new issue',
279 303 :description => 'This is the description',
280 304 :priority_id => 5}
281 305 assert_redirected_to 'issues/show'
282 306 end
283 307
284 308 def test_post_new_with_required_custom_field_and_without_custom_fields_param
285 309 field = IssueCustomField.find_by_name('Database')
286 310 field.update_attribute(:is_required, true)
287 311
288 312 @request.session[:user_id] = 2
289 313 post :new, :project_id => 1,
290 314 :issue => {:tracker_id => 1,
291 315 :subject => 'This is the test_new issue',
292 316 :description => 'This is the description',
293 317 :priority_id => 5}
294 318 assert_response :success
295 319 assert_template 'new'
296 320 issue = assigns(:issue)
297 321 assert_not_nil issue
298 322 assert_equal 'activerecord_error_invalid', issue.errors.on(:custom_values)
299 323 end
300 324
301 325 def test_post_should_preserve_fields_values_on_validation_failure
302 326 @request.session[:user_id] = 2
303 327 post :new, :project_id => 1,
304 328 :issue => {:tracker_id => 1,
305 329 :subject => 'This is the test_new issue',
306 330 # empty description
307 331 :description => '',
308 332 :priority_id => 6,
309 333 :custom_field_values => {'1' => 'Oracle', '2' => 'Value for field 2'}}
310 334 assert_response :success
311 335 assert_template 'new'
312 336
313 337 assert_tag :input, :attributes => { :name => 'issue[subject]',
314 338 :value => 'This is the test_new issue' }
315 339 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
316 340 :child => { :tag => 'option', :attributes => { :selected => 'selected',
317 341 :value => '6' },
318 342 :content => 'High' }
319 343 # Custom fields
320 344 assert_tag :select, :attributes => { :name => 'issue[custom_field_values][1]' },
321 345 :child => { :tag => 'option', :attributes => { :selected => 'selected',
322 346 :value => 'Oracle' },
323 347 :content => 'Oracle' }
324 348 assert_tag :input, :attributes => { :name => 'issue[custom_field_values][2]',
325 349 :value => 'Value for field 2'}
326 350 end
327 351
328 352 def test_copy_issue
329 353 @request.session[:user_id] = 2
330 354 get :new, :project_id => 1, :copy_from => 1
331 355 assert_template 'new'
332 356 assert_not_nil assigns(:issue)
333 357 orig = Issue.find(1)
334 358 assert_equal orig.subject, assigns(:issue).subject
335 359 end
336 360
337 361 def test_get_edit
338 362 @request.session[:user_id] = 2
339 363 get :edit, :id => 1
340 364 assert_response :success
341 365 assert_template 'edit'
342 366 assert_not_nil assigns(:issue)
343 367 assert_equal Issue.find(1), assigns(:issue)
344 368 end
345 369
346 370 def test_get_edit_with_params
347 371 @request.session[:user_id] = 2
348 372 get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 }
349 373 assert_response :success
350 374 assert_template 'edit'
351 375
352 376 issue = assigns(:issue)
353 377 assert_not_nil issue
354 378
355 379 assert_equal 5, issue.status_id
356 380 assert_tag :select, :attributes => { :name => 'issue[status_id]' },
357 381 :child => { :tag => 'option',
358 382 :content => 'Closed',
359 383 :attributes => { :selected => 'selected' } }
360 384
361 385 assert_equal 7, issue.priority_id
362 386 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
363 387 :child => { :tag => 'option',
364 388 :content => 'Urgent',
365 389 :attributes => { :selected => 'selected' } }
366 390 end
367 391
368 392 def test_reply_to_issue
369 393 @request.session[:user_id] = 2
370 394 get :reply, :id => 1
371 395 assert_response :success
372 396 assert_select_rjs :show, "update"
373 397 end
374 398
375 399 def test_reply_to_note
376 400 @request.session[:user_id] = 2
377 401 get :reply, :id => 1, :journal_id => 2
378 402 assert_response :success
379 403 assert_select_rjs :show, "update"
380 404 end
381 405
382 406 def test_post_edit_without_custom_fields_param
383 407 @request.session[:user_id] = 2
384 408 ActionMailer::Base.deliveries.clear
385 409
386 410 issue = Issue.find(1)
387 411 assert_equal '125', issue.custom_value_for(2).value
388 412 old_subject = issue.subject
389 413 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
390 414
391 415 assert_difference('Journal.count') do
392 416 assert_difference('JournalDetail.count', 2) do
393 417 post :edit, :id => 1, :issue => {:subject => new_subject,
394 418 :priority_id => '6',
395 419 :category_id => '1' # no change
396 420 }
397 421 end
398 422 end
399 423 assert_redirected_to 'issues/show/1'
400 424 issue.reload
401 425 assert_equal new_subject, issue.subject
402 426 # Make sure custom fields were not cleared
403 427 assert_equal '125', issue.custom_value_for(2).value
404 428
405 429 mail = ActionMailer::Base.deliveries.last
406 430 assert_kind_of TMail::Mail, mail
407 431 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
408 432 assert mail.body.include?("Subject changed from #{old_subject} to #{new_subject}")
409 433 end
410 434
411 435 def test_post_edit_with_custom_field_change
412 436 @request.session[:user_id] = 2
413 437 issue = Issue.find(1)
414 438 assert_equal '125', issue.custom_value_for(2).value
415 439
416 440 assert_difference('Journal.count') do
417 441 assert_difference('JournalDetail.count', 3) do
418 442 post :edit, :id => 1, :issue => {:subject => 'Custom field change',
419 443 :priority_id => '6',
420 444 :category_id => '1', # no change
421 445 :custom_field_values => { '2' => 'New custom value' }
422 446 }
423 447 end
424 448 end
425 449 assert_redirected_to 'issues/show/1'
426 450 issue.reload
427 451 assert_equal 'New custom value', issue.custom_value_for(2).value
428 452
429 453 mail = ActionMailer::Base.deliveries.last
430 454 assert_kind_of TMail::Mail, mail
431 455 assert mail.body.include?("Searchable field changed from 125 to New custom value")
432 456 end
433 457
434 458 def test_post_edit_with_status_and_assignee_change
435 459 issue = Issue.find(1)
436 460 assert_equal 1, issue.status_id
437 461 @request.session[:user_id] = 2
438 462 assert_difference('TimeEntry.count', 0) do
439 463 post :edit,
440 464 :id => 1,
441 465 :issue => { :status_id => 2, :assigned_to_id => 3 },
442 466 :notes => 'Assigned to dlopper',
443 467 :time_entry => { :hours => '', :comments => '', :activity_id => Enumeration.get_values('ACTI').first }
444 468 end
445 469 assert_redirected_to 'issues/show/1'
446 470 issue.reload
447 471 assert_equal 2, issue.status_id
448 472 j = issue.journals.find(:first, :order => 'id DESC')
449 473 assert_equal 'Assigned to dlopper', j.notes
450 474 assert_equal 2, j.details.size
451 475
452 476 mail = ActionMailer::Base.deliveries.last
453 477 assert mail.body.include?("Status changed from New to Assigned")
454 478 end
455 479
456 480 def test_post_edit_with_note_only
457 481 notes = 'Note added by IssuesControllerTest#test_update_with_note_only'
458 482 # anonymous user
459 483 post :edit,
460 484 :id => 1,
461 485 :notes => notes
462 486 assert_redirected_to 'issues/show/1'
463 487 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
464 488 assert_equal notes, j.notes
465 489 assert_equal 0, j.details.size
466 490 assert_equal User.anonymous, j.user
467 491
468 492 mail = ActionMailer::Base.deliveries.last
469 493 assert mail.body.include?(notes)
470 494 end
471 495
472 496 def test_post_edit_with_note_and_spent_time
473 497 @request.session[:user_id] = 2
474 498 spent_hours_before = Issue.find(1).spent_hours
475 499 assert_difference('TimeEntry.count') do
476 500 post :edit,
477 501 :id => 1,
478 502 :notes => '2.5 hours added',
479 503 :time_entry => { :hours => '2.5', :comments => '', :activity_id => Enumeration.get_values('ACTI').first }
480 504 end
481 505 assert_redirected_to 'issues/show/1'
482 506
483 507 issue = Issue.find(1)
484 508
485 509 j = issue.journals.find(:first, :order => 'id DESC')
486 510 assert_equal '2.5 hours added', j.notes
487 511 assert_equal 0, j.details.size
488 512
489 513 t = issue.time_entries.find(:first, :order => 'id DESC')
490 514 assert_not_nil t
491 515 assert_equal 2.5, t.hours
492 516 assert_equal spent_hours_before + 2.5, issue.spent_hours
493 517 end
494 518
495 519 def test_post_edit_with_attachment_only
496 520 set_tmp_attachments_directory
497 521
498 522 # Delete all fixtured journals, a race condition can occur causing the wrong
499 523 # journal to get fetched in the next find.
500 524 Journal.delete_all
501 525
502 526 # anonymous user
503 527 post :edit,
504 528 :id => 1,
505 529 :notes => '',
506 530 :attachments => {'1' => {'file' => test_uploaded_file('testfile.txt', 'text/plain')}}
507 531 assert_redirected_to 'issues/show/1'
508 532 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
509 533 assert j.notes.blank?
510 534 assert_equal 1, j.details.size
511 535 assert_equal 'testfile.txt', j.details.first.value
512 536 assert_equal User.anonymous, j.user
513 537
514 538 mail = ActionMailer::Base.deliveries.last
515 539 assert mail.body.include?('testfile.txt')
516 540 end
517 541
518 542 def test_post_edit_with_no_change
519 543 issue = Issue.find(1)
520 544 issue.journals.clear
521 545 ActionMailer::Base.deliveries.clear
522 546
523 547 post :edit,
524 548 :id => 1,
525 549 :notes => ''
526 550 assert_redirected_to 'issues/show/1'
527 551
528 552 issue.reload
529 553 assert issue.journals.empty?
530 554 # No email should be sent
531 555 assert ActionMailer::Base.deliveries.empty?
532 556 end
533 557
534 558 def test_bulk_edit
535 559 @request.session[:user_id] = 2
536 560 # update issues priority
537 561 post :bulk_edit, :ids => [1, 2], :priority_id => 7, :notes => 'Bulk editing', :assigned_to_id => ''
538 562 assert_response 302
539 563 # check that the issues were updated
540 564 assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id}
541 565 assert_equal 'Bulk editing', Issue.find(1).journals.find(:first, :order => 'created_on DESC').notes
542 566 end
543 567
544 568 def test_bulk_unassign
545 569 assert_not_nil Issue.find(2).assigned_to
546 570 @request.session[:user_id] = 2
547 571 # unassign issues
548 572 post :bulk_edit, :ids => [1, 2], :notes => 'Bulk unassigning', :assigned_to_id => 'none'
549 573 assert_response 302
550 574 # check that the issues were updated
551 575 assert_nil Issue.find(2).assigned_to
552 576 end
553 577
554 578 def test_move_one_issue_to_another_project
555 579 @request.session[:user_id] = 1
556 580 post :move, :id => 1, :new_project_id => 2
557 581 assert_redirected_to 'projects/ecookbook/issues'
558 582 assert_equal 2, Issue.find(1).project_id
559 583 end
560 584
561 585 def test_bulk_move_to_another_project
562 586 @request.session[:user_id] = 1
563 587 post :move, :ids => [1, 2], :new_project_id => 2
564 588 assert_redirected_to 'projects/ecookbook/issues'
565 589 # Issues moved to project 2
566 590 assert_equal 2, Issue.find(1).project_id
567 591 assert_equal 2, Issue.find(2).project_id
568 592 # No tracker change
569 593 assert_equal 1, Issue.find(1).tracker_id
570 594 assert_equal 2, Issue.find(2).tracker_id
571 595 end
572 596
573 597 def test_bulk_move_to_another_tracker
574 598 @request.session[:user_id] = 1
575 599 post :move, :ids => [1, 2], :new_tracker_id => 2
576 600 assert_redirected_to 'projects/ecookbook/issues'
577 601 assert_equal 2, Issue.find(1).tracker_id
578 602 assert_equal 2, Issue.find(2).tracker_id
579 603 end
580 604
581 605 def test_context_menu_one_issue
582 606 @request.session[:user_id] = 2
583 607 get :context_menu, :ids => [1]
584 608 assert_response :success
585 609 assert_template 'context_menu'
586 610 assert_tag :tag => 'a', :content => 'Edit',
587 611 :attributes => { :href => '/issues/edit/1',
588 612 :class => 'icon-edit' }
589 613 assert_tag :tag => 'a', :content => 'Closed',
590 614 :attributes => { :href => '/issues/edit/1?issue%5Bstatus_id%5D=5',
591 615 :class => '' }
592 616 assert_tag :tag => 'a', :content => 'Immediate',
593 617 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;priority_id=8',
594 618 :class => '' }
595 619 assert_tag :tag => 'a', :content => 'Dave Lopper',
596 620 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1',
597 621 :class => '' }
598 622 assert_tag :tag => 'a', :content => 'Copy',
599 623 :attributes => { :href => '/projects/ecookbook/issues/new?copy_from=1',
600 624 :class => 'icon-copy' }
601 625 assert_tag :tag => 'a', :content => 'Move',
602 626 :attributes => { :href => '/issues/move?ids%5B%5D=1',
603 627 :class => 'icon-move' }
604 628 assert_tag :tag => 'a', :content => 'Delete',
605 629 :attributes => { :href => '/issues/destroy?ids%5B%5D=1',
606 630 :class => 'icon-del' }
607 631 end
608 632
609 633 def test_context_menu_one_issue_by_anonymous
610 634 get :context_menu, :ids => [1]
611 635 assert_response :success
612 636 assert_template 'context_menu'
613 637 assert_tag :tag => 'a', :content => 'Delete',
614 638 :attributes => { :href => '#',
615 639 :class => 'icon-del disabled' }
616 640 end
617 641
618 642 def test_context_menu_multiple_issues_of_same_project
619 643 @request.session[:user_id] = 2
620 644 get :context_menu, :ids => [1, 2]
621 645 assert_response :success
622 646 assert_template 'context_menu'
623 647 assert_tag :tag => 'a', :content => 'Edit',
624 648 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2',
625 649 :class => 'icon-edit' }
626 650 assert_tag :tag => 'a', :content => 'Immediate',
627 651 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2&amp;priority_id=8',
628 652 :class => '' }
629 653 assert_tag :tag => 'a', :content => 'Dave Lopper',
630 654 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1&amp;ids%5B%5D=2',
631 655 :class => '' }
632 656 assert_tag :tag => 'a', :content => 'Move',
633 657 :attributes => { :href => '/issues/move?ids%5B%5D=1&amp;ids%5B%5D=2',
634 658 :class => 'icon-move' }
635 659 assert_tag :tag => 'a', :content => 'Delete',
636 660 :attributes => { :href => '/issues/destroy?ids%5B%5D=1&amp;ids%5B%5D=2',
637 661 :class => 'icon-del' }
638 662 end
639 663
640 664 def test_context_menu_multiple_issues_of_different_project
641 665 @request.session[:user_id] = 2
642 666 get :context_menu, :ids => [1, 2, 4]
643 667 assert_response :success
644 668 assert_template 'context_menu'
645 669 assert_tag :tag => 'a', :content => 'Delete',
646 670 :attributes => { :href => '#',
647 671 :class => 'icon-del disabled' }
648 672 end
649 673
650 674 def test_destroy_issue_with_no_time_entries
651 675 assert_nil TimeEntry.find_by_issue_id(2)
652 676 @request.session[:user_id] = 2
653 677 post :destroy, :id => 2
654 678 assert_redirected_to 'projects/ecookbook/issues'
655 679 assert_nil Issue.find_by_id(2)
656 680 end
657 681
658 682 def test_destroy_issues_with_time_entries
659 683 @request.session[:user_id] = 2
660 684 post :destroy, :ids => [1, 3]
661 685 assert_response :success
662 686 assert_template 'destroy'
663 687 assert_not_nil assigns(:hours)
664 688 assert Issue.find_by_id(1) && Issue.find_by_id(3)
665 689 end
666 690
667 691 def test_destroy_issues_and_destroy_time_entries
668 692 @request.session[:user_id] = 2
669 693 post :destroy, :ids => [1, 3], :todo => 'destroy'
670 694 assert_redirected_to 'projects/ecookbook/issues'
671 695 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
672 696 assert_nil TimeEntry.find_by_id([1, 2])
673 697 end
674 698
675 699 def test_destroy_issues_and_assign_time_entries_to_project
676 700 @request.session[:user_id] = 2
677 701 post :destroy, :ids => [1, 3], :todo => 'nullify'
678 702 assert_redirected_to 'projects/ecookbook/issues'
679 703 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
680 704 assert_nil TimeEntry.find(1).issue_id
681 705 assert_nil TimeEntry.find(2).issue_id
682 706 end
683 707
684 708 def test_destroy_issues_and_reassign_time_entries_to_another_issue
685 709 @request.session[:user_id] = 2
686 710 post :destroy, :ids => [1, 3], :todo => 'reassign', :reassign_to_id => 2
687 711 assert_redirected_to 'projects/ecookbook/issues'
688 712 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
689 713 assert_equal 2, TimeEntry.find(1).issue_id
690 714 assert_equal 2, TimeEntry.find(2).issue_id
691 715 end
692 716
693 717 def test_destroy_attachment
694 718 issue = Issue.find(3)
695 719 a = issue.attachments.size
696 720 @request.session[:user_id] = 2
697 721 post :destroy_attachment, :id => 3, :attachment_id => 1
698 722 assert_redirected_to 'issues/show/3'
699 723 assert_nil Attachment.find_by_id(1)
700 724 issue.reload
701 725 assert_equal((a-1), issue.attachments.size)
702 726 j = issue.journals.find(:first, :order => 'created_on DESC')
703 727 assert_equal 'attachment', j.details.first.property
704 728 end
705 729 end
General Comments 0
You need to be logged in to leave comments. Login now