##// END OF EJS Templates
Move PDF stuff to a single helper....
Jean-Philippe Lang -
r2224:ceb2320ef078
parent child
Show More
@@ -0,0 +1,438
1 # Redmine - project management software
2 # Copyright (C) 2006-2009 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 require 'iconv'
19 require 'rfpdf/chinese'
20
21 module Redmine
22 module Export
23 module PDF
24 class IFPDF < FPDF
25 include GLoc
26 attr_accessor :footer_date
27
28 def initialize(lang)
29 super()
30 set_language_if_valid lang
31 case current_language.to_s
32 when 'ja'
33 extend(PDF_Japanese)
34 AddSJISFont()
35 @font_for_content = 'SJIS'
36 @font_for_footer = 'SJIS'
37 when 'zh'
38 extend(PDF_Chinese)
39 AddGBFont()
40 @font_for_content = 'GB'
41 @font_for_footer = 'GB'
42 when 'zh-tw'
43 extend(PDF_Chinese)
44 AddBig5Font()
45 @font_for_content = 'Big5'
46 @font_for_footer = 'Big5'
47 else
48 @font_for_content = 'Arial'
49 @font_for_footer = 'Helvetica'
50 end
51 SetCreator(Redmine::Info.app_name)
52 SetFont(@font_for_content)
53 end
54
55 def SetFontStyle(style, size)
56 SetFont(@font_for_content, style, size)
57 end
58
59 def Cell(w,h=0,txt='',border=0,ln=0,align='',fill=0,link='')
60 @ic ||= Iconv.new(l(:general_pdf_encoding), 'UTF-8')
61 # these quotation marks are not correctly rendered in the pdf
62 txt = txt.gsub(/[“�]/, '"') if txt
63 txt = begin
64 # 0x5c char handling
65 txtar = txt.split('\\')
66 txtar << '' if txt[-1] == ?\\
67 txtar.collect {|x| @ic.iconv(x)}.join('\\').gsub(/\\/, "\\\\\\\\")
68 rescue
69 txt
70 end || ''
71 super w,h,txt,border,ln,align,fill,link
72 end
73
74 def Footer
75 SetFont(@font_for_footer, 'I', 8)
76 SetY(-15)
77 SetX(15)
78 Cell(0, 5, @footer_date, 0, 0, 'L')
79 SetY(-15)
80 SetX(-30)
81 Cell(0, 5, PageNo().to_s + '/{nb}', 0, 0, 'C')
82 end
83 end
84
85 # Returns a PDF string of a list of issues
86 def issues_to_pdf(issues, project)
87 pdf = IFPDF.new(current_language)
88 title = project ? "#{project} - #{l(:label_issue_plural)}" : "#{l(:label_issue_plural)}"
89 pdf.SetTitle(title)
90 pdf.AliasNbPages
91 pdf.footer_date = format_date(Date.today)
92 pdf.AddPage("L")
93 row_height = 7
94
95 # title
96 pdf.SetFontStyle('B',11)
97 pdf.Cell(190,10, title)
98 pdf.Ln
99
100 # headers
101 pdf.SetFontStyle('B',10)
102 pdf.SetFillColor(230, 230, 230)
103 pdf.Cell(15, row_height, "#", 0, 0, 'L', 1)
104 pdf.Cell(30, row_height, l(:field_tracker), 0, 0, 'L', 1)
105 pdf.Cell(30, row_height, l(:field_status), 0, 0, 'L', 1)
106 pdf.Cell(30, row_height, l(:field_priority), 0, 0, 'L', 1)
107 pdf.Cell(40, row_height, l(:field_assigned_to), 0, 0, 'L', 1)
108 pdf.Cell(25, row_height, l(:field_updated_on), 0, 0, 'L', 1)
109 pdf.Cell(0, row_height, l(:field_subject), 0, 0, 'L', 1)
110 pdf.Line(10, pdf.GetY, 287, pdf.GetY)
111 pdf.Ln
112 pdf.Line(10, pdf.GetY, 287, pdf.GetY)
113 pdf.SetY(pdf.GetY() + 1)
114
115 # rows
116 pdf.SetFontStyle('',9)
117 pdf.SetFillColor(255, 255, 255)
118 issues.each do |issue|
119 pdf.Cell(15, row_height, issue.id.to_s, 0, 0, 'L', 1)
120 pdf.Cell(30, row_height, issue.tracker.name, 0, 0, 'L', 1)
121 pdf.Cell(30, row_height, issue.status.name, 0, 0, 'L', 1)
122 pdf.Cell(30, row_height, issue.priority.name, 0, 0, 'L', 1)
123 pdf.Cell(40, row_height, issue.assigned_to ? issue.assigned_to.to_s : '', 0, 0, 'L', 1)
124 pdf.Cell(25, row_height, format_date(issue.updated_on), 0, 0, 'L', 1)
125 pdf.MultiCell(0, row_height, (project == issue.project ? issue.subject : "#{issue.project} - #{issue.subject}"))
126 pdf.Line(10, pdf.GetY, 287, pdf.GetY)
127 pdf.SetY(pdf.GetY() + 1)
128 end
129 pdf.Output
130 end
131
132 # Returns a PDF string of a single issue
133 def issue_to_pdf(issue)
134 pdf = IFPDF.new(current_language)
135 pdf.SetTitle("#{issue.project} - ##{issue.tracker} #{issue.id}")
136 pdf.AliasNbPages
137 pdf.footer_date = format_date(Date.today)
138 pdf.AddPage
139
140 pdf.SetFontStyle('B',11)
141 pdf.Cell(190,10, "#{issue.project} - #{issue.tracker} # #{issue.id}: #{issue.subject}")
142 pdf.Ln
143
144 y0 = pdf.GetY
145
146 pdf.SetFontStyle('B',9)
147 pdf.Cell(35,5, l(:field_status) + ":","LT")
148 pdf.SetFontStyle('',9)
149 pdf.Cell(60,5, issue.status.to_s,"RT")
150 pdf.SetFontStyle('B',9)
151 pdf.Cell(35,5, l(:field_priority) + ":","LT")
152 pdf.SetFontStyle('',9)
153 pdf.Cell(60,5, issue.priority.to_s,"RT")
154 pdf.Ln
155
156 pdf.SetFontStyle('B',9)
157 pdf.Cell(35,5, l(:field_author) + ":","L")
158 pdf.SetFontStyle('',9)
159 pdf.Cell(60,5, issue.author.to_s,"R")
160 pdf.SetFontStyle('B',9)
161 pdf.Cell(35,5, l(:field_category) + ":","L")
162 pdf.SetFontStyle('',9)
163 pdf.Cell(60,5, issue.category.to_s,"R")
164 pdf.Ln
165
166 pdf.SetFontStyle('B',9)
167 pdf.Cell(35,5, l(:field_created_on) + ":","L")
168 pdf.SetFontStyle('',9)
169 pdf.Cell(60,5, format_date(issue.created_on),"R")
170 pdf.SetFontStyle('B',9)
171 pdf.Cell(35,5, l(:field_assigned_to) + ":","L")
172 pdf.SetFontStyle('',9)
173 pdf.Cell(60,5, issue.assigned_to.to_s,"R")
174 pdf.Ln
175
176 pdf.SetFontStyle('B',9)
177 pdf.Cell(35,5, l(:field_updated_on) + ":","LB")
178 pdf.SetFontStyle('',9)
179 pdf.Cell(60,5, format_date(issue.updated_on),"RB")
180 pdf.SetFontStyle('B',9)
181 pdf.Cell(35,5, l(:field_due_date) + ":","LB")
182 pdf.SetFontStyle('',9)
183 pdf.Cell(60,5, format_date(issue.due_date),"RB")
184 pdf.Ln
185
186 for custom_value in issue.custom_values
187 pdf.SetFontStyle('B',9)
188 pdf.Cell(35,5, custom_value.custom_field.name + ":","L")
189 pdf.SetFontStyle('',9)
190 pdf.MultiCell(155,5, (show_value custom_value),"R")
191 end
192
193 pdf.SetFontStyle('B',9)
194 pdf.Cell(35,5, l(:field_subject) + ":","LTB")
195 pdf.SetFontStyle('',9)
196 pdf.Cell(155,5, issue.subject,"RTB")
197 pdf.Ln
198
199 pdf.SetFontStyle('B',9)
200 pdf.Cell(35,5, l(:field_description) + ":")
201 pdf.SetFontStyle('',9)
202 pdf.MultiCell(155,5, @issue.description,"BR")
203
204 pdf.Line(pdf.GetX, y0, pdf.GetX, pdf.GetY)
205 pdf.Line(pdf.GetX, pdf.GetY, 170, pdf.GetY)
206 pdf.Ln
207
208 if issue.changesets.any? && User.current.allowed_to?(:view_changesets, issue.project)
209 pdf.SetFontStyle('B',9)
210 pdf.Cell(190,5, l(:label_associated_revisions), "B")
211 pdf.Ln
212 for changeset in issue.changesets
213 pdf.SetFontStyle('B',8)
214 pdf.Cell(190,5, format_time(changeset.committed_on) + " - " + changeset.author.to_s)
215 pdf.Ln
216 unless changeset.comments.blank?
217 pdf.SetFontStyle('',8)
218 pdf.MultiCell(190,5, changeset.comments)
219 end
220 pdf.Ln
221 end
222 end
223
224 pdf.SetFontStyle('B',9)
225 pdf.Cell(190,5, l(:label_history), "B")
226 pdf.Ln
227 for journal in issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
228 pdf.SetFontStyle('B',8)
229 pdf.Cell(190,5, format_time(journal.created_on) + " - " + journal.user.name)
230 pdf.Ln
231 pdf.SetFontStyle('I',8)
232 for detail in journal.details
233 pdf.Cell(190,5, "- " + show_detail(detail, true))
234 pdf.Ln
235 end
236 if journal.notes?
237 pdf.SetFontStyle('',8)
238 pdf.MultiCell(190,5, journal.notes)
239 end
240 pdf.Ln
241 end
242
243 if issue.attachments.any?
244 pdf.SetFontStyle('B',9)
245 pdf.Cell(190,5, l(:label_attachment_plural), "B")
246 pdf.Ln
247 for attachment in issue.attachments
248 pdf.SetFontStyle('',8)
249 pdf.Cell(80,5, attachment.filename)
250 pdf.Cell(20,5, number_to_human_size(attachment.filesize),0,0,"R")
251 pdf.Cell(25,5, format_date(attachment.created_on),0,0,"R")
252 pdf.Cell(65,5, attachment.author.name,0,0,"R")
253 pdf.Ln
254 end
255 end
256 pdf.Output
257 end
258
259 # Returns a PDF string of a gantt chart
260 def gantt_to_pdf(gantt, project)
261 pdf = IFPDF.new(current_language)
262 pdf.SetTitle("#{l(:label_gantt)} #{project}")
263 pdf.AliasNbPages
264 pdf.footer_date = format_date(Date.today)
265 pdf.AddPage("L")
266 pdf.SetFontStyle('B',12)
267 pdf.SetX(15)
268 pdf.Cell(70, 20, project.to_s)
269 pdf.Ln
270 pdf.SetFontStyle('B',9)
271
272 subject_width = 70
273 header_heigth = 5
274
275 headers_heigth = header_heigth
276 show_weeks = false
277 show_days = false
278
279 if gantt.months < 7
280 show_weeks = true
281 headers_heigth = 2*header_heigth
282 if gantt.months < 3
283 show_days = true
284 headers_heigth = 3*header_heigth
285 end
286 end
287
288 g_width = 210
289 zoom = (g_width) / (gantt.date_to - gantt.date_from + 1)
290 g_height = 120
291 t_height = g_height + headers_heigth
292
293 y_start = pdf.GetY
294
295 # Months headers
296 month_f = gantt.date_from
297 left = subject_width
298 height = header_heigth
299 gantt.months.times do
300 width = ((month_f >> 1) - month_f) * zoom
301 pdf.SetY(y_start)
302 pdf.SetX(left)
303 pdf.Cell(width, height, "#{month_f.year}-#{month_f.month}", "LTR", 0, "C")
304 left = left + width
305 month_f = month_f >> 1
306 end
307
308 # Weeks headers
309 if show_weeks
310 left = subject_width
311 height = header_heigth
312 if gantt.date_from.cwday == 1
313 # gantt.date_from is monday
314 week_f = gantt.date_from
315 else
316 # find next monday after gantt.date_from
317 week_f = gantt.date_from + (7 - gantt.date_from.cwday + 1)
318 width = (7 - gantt.date_from.cwday + 1) * zoom-1
319 pdf.SetY(y_start + header_heigth)
320 pdf.SetX(left)
321 pdf.Cell(width + 1, height, "", "LTR")
322 left = left + width+1
323 end
324 while week_f <= gantt.date_to
325 width = (week_f + 6 <= gantt.date_to) ? 7 * zoom : (gantt.date_to - week_f + 1) * zoom
326 pdf.SetY(y_start + header_heigth)
327 pdf.SetX(left)
328 pdf.Cell(width, height, (width >= 5 ? week_f.cweek.to_s : ""), "LTR", 0, "C")
329 left = left + width
330 week_f = week_f+7
331 end
332 end
333
334 # Days headers
335 if show_days
336 left = subject_width
337 height = header_heigth
338 wday = gantt.date_from.cwday
339 pdf.SetFontStyle('B',7)
340 (gantt.date_to - gantt.date_from + 1).to_i.times do
341 width = zoom
342 pdf.SetY(y_start + 2 * header_heigth)
343 pdf.SetX(left)
344 pdf.Cell(width, height, day_name(wday).first, "LTR", 0, "C")
345 left = left + width
346 wday = wday + 1
347 wday = 1 if wday > 7
348 end
349 end
350
351 pdf.SetY(y_start)
352 pdf.SetX(15)
353 pdf.Cell(subject_width+g_width-15, headers_heigth, "", 1)
354
355 # Tasks
356 top = headers_heigth + y_start
357 pdf.SetFontStyle('B',7)
358 gantt.events.each do |i|
359 pdf.SetY(top)
360 pdf.SetX(15)
361
362 if i.is_a? Issue
363 pdf.Cell(subject_width-15, 5, "#{i.tracker} #{i.id}: #{i.subject}".sub(/^(.{30}[^\s]*\s).*$/, '\1 (...)'), "LR")
364 else
365 pdf.Cell(subject_width-15, 5, "#{l(:label_version)}: #{i.name}", "LR")
366 end
367
368 pdf.SetY(top)
369 pdf.SetX(subject_width)
370 pdf.Cell(g_width, 5, "", "LR")
371
372 pdf.SetY(top+1.5)
373
374 if i.is_a? Issue
375 i_start_date = (i.start_date >= gantt.date_from ? i.start_date : gantt.date_from )
376 i_end_date = (i.due_before <= gantt.date_to ? i.due_before : gantt.date_to )
377
378 i_done_date = i.start_date + ((i.due_before - i.start_date+1)*i.done_ratio/100).floor
379 i_done_date = (i_done_date <= gantt.date_from ? gantt.date_from : i_done_date )
380 i_done_date = (i_done_date >= gantt.date_to ? gantt.date_to : i_done_date )
381
382 i_late_date = [i_end_date, Date.today].min if i_start_date < Date.today
383
384 i_left = ((i_start_date - gantt.date_from)*zoom)
385 i_width = ((i_end_date - i_start_date + 1)*zoom)
386 d_width = ((i_done_date - i_start_date)*zoom)
387 l_width = ((i_late_date - i_start_date+1)*zoom) if i_late_date
388 l_width ||= 0
389
390 pdf.SetX(subject_width + i_left)
391 pdf.SetFillColor(200,200,200)
392 pdf.Cell(i_width, 2, "", 0, 0, "", 1)
393
394 if l_width > 0
395 pdf.SetY(top+1.5)
396 pdf.SetX(subject_width + i_left)
397 pdf.SetFillColor(255,100,100)
398 pdf.Cell(l_width, 2, "", 0, 0, "", 1)
399 end
400 if d_width > 0
401 pdf.SetY(top+1.5)
402 pdf.SetX(subject_width + i_left)
403 pdf.SetFillColor(100,100,255)
404 pdf.Cell(d_width, 2, "", 0, 0, "", 1)
405 end
406
407 pdf.SetY(top+1.5)
408 pdf.SetX(subject_width + i_left + i_width)
409 pdf.Cell(30, 2, "#{i.status} #{i.done_ratio}%")
410 else
411 i_left = ((i.start_date - gantt.date_from)*zoom)
412
413 pdf.SetX(subject_width + i_left)
414 pdf.SetFillColor(50,200,50)
415 pdf.Cell(2, 2, "", 0, 0, "", 1)
416
417 pdf.SetY(top+1.5)
418 pdf.SetX(subject_width + i_left + 3)
419 pdf.Cell(30, 2, "#{i.name}")
420 end
421
422 top = top + 5
423 pdf.SetDrawColor(200, 200, 200)
424 pdf.Line(15, top, subject_width+g_width, top)
425 if pdf.GetY() > 180
426 pdf.AddPage("L")
427 top = 20
428 pdf.Line(15, top, subject_width+g_width, top)
429 end
430 pdf.SetDrawColor(0, 0, 0)
431 end
432
433 pdf.Line(15, top, subject_width+g_width, top)
434 pdf.Output
435 end
436 end
437 end
438 end
@@ -1,487 +1,486
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]
22 22 before_filter :find_issues, :only => [:bulk_edit, :move, :destroy]
23 23 before_filter :find_project, :only => [:new, :update_form, :preview]
24 24 before_filter :authorize, :except => [:index, :changes, :gantt, :calendar, :preview, :update_form, :context_menu]
25 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 helper :ifpdf
34 include IfpdfHelper
35 33 helper :issue_relations
36 34 include IssueRelationsHelper
37 35 helper :watchers
38 36 include WatchersHelper
39 37 helper :attachments
40 38 include AttachmentsHelper
41 39 helper :queries
42 40 helper :sort
43 41 include SortHelper
44 42 include IssuesHelper
45 43 helper :timelog
44 include Redmine::Export::PDF
46 45
47 46 def index
48 47 retrieve_query
49 48 sort_init 'id', 'desc'
50 49 sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
51 50
52 51 if @query.valid?
53 52 limit = per_page_option
54 53 respond_to do |format|
55 54 format.html { }
56 55 format.atom { }
57 56 format.csv { limit = Setting.issues_export_limit.to_i }
58 57 format.pdf { limit = Setting.issues_export_limit.to_i }
59 58 end
60 59 @issue_count = Issue.count(:include => [:status, :project], :conditions => @query.statement)
61 60 @issue_pages = Paginator.new self, @issue_count, limit, params['page']
62 61 @issues = Issue.find :all, :order => sort_clause,
63 62 :include => [ :assigned_to, :status, :tracker, :project, :priority, :category, :fixed_version ],
64 63 :conditions => @query.statement,
65 64 :limit => limit,
66 65 :offset => @issue_pages.current.offset
67 66 respond_to do |format|
68 67 format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
69 68 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
70 69 format.csv { send_data(issues_to_csv(@issues, @project).read, :type => 'text/csv; header=present', :filename => 'export.csv') }
71 format.pdf { send_data(render(:template => 'issues/index.rfpdf', :layout => false), :type => 'application/pdf', :filename => 'export.pdf') }
70 format.pdf { send_data(issues_to_pdf(@issues, @project), :type => 'application/pdf', :filename => 'export.pdf') }
72 71 end
73 72 else
74 73 # Send html if the query is not valid
75 74 render(:template => 'issues/index.rhtml', :layout => !request.xhr?)
76 75 end
77 76 rescue ActiveRecord::RecordNotFound
78 77 render_404
79 78 end
80 79
81 80 def changes
82 81 retrieve_query
83 82 sort_init 'id', 'desc'
84 83 sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
85 84
86 85 if @query.valid?
87 86 @journals = Journal.find :all, :include => [ :details, :user, {:issue => [:project, :author, :tracker, :status]} ],
88 87 :conditions => @query.statement,
89 88 :limit => 25,
90 89 :order => "#{Journal.table_name}.created_on DESC"
91 90 end
92 91 @title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
93 92 render :layout => false, :content_type => 'application/atom+xml'
94 93 rescue ActiveRecord::RecordNotFound
95 94 render_404
96 95 end
97 96
98 97 def show
99 98 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
100 99 @journals.each_with_index {|j,i| j.indice = i+1}
101 100 @journals.reverse! if User.current.wants_comments_in_reverse_order?
102 101 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
103 102 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
104 103 @priorities = Enumeration::get_values('IPRI')
105 104 @time_entry = TimeEntry.new
106 105 respond_to do |format|
107 106 format.html { render :template => 'issues/show.rhtml' }
108 107 format.atom { render :action => 'changes', :layout => false, :content_type => 'application/atom+xml' }
109 format.pdf { send_data(render(:template => 'issues/show.rfpdf', :layout => false), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
108 format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
110 109 end
111 110 end
112 111
113 112 # Add a new issue
114 113 # The new issue will be created from an existing one if copy_from parameter is given
115 114 def new
116 115 @issue = Issue.new
117 116 @issue.copy_from(params[:copy_from]) if params[:copy_from]
118 117 @issue.project = @project
119 118 # Tracker must be set before custom field values
120 119 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
121 120 if @issue.tracker.nil?
122 121 flash.now[:error] = 'No tracker is associated to this project. Please check the Project settings.'
123 122 render :nothing => true, :layout => true
124 123 return
125 124 end
126 125 if params[:issue].is_a?(Hash)
127 126 @issue.attributes = params[:issue]
128 127 @issue.watcher_user_ids = params[:issue]['watcher_user_ids'] if User.current.allowed_to?(:add_issue_watchers, @project)
129 128 end
130 129 @issue.author = User.current
131 130
132 131 default_status = IssueStatus.default
133 132 unless default_status
134 133 flash.now[:error] = 'No default issue status is defined. Please check your configuration (Go to "Administration -> Issue statuses").'
135 134 render :nothing => true, :layout => true
136 135 return
137 136 end
138 137 @issue.status = default_status
139 138 @allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(User.current.role_for_project(@project), @issue.tracker)).uniq
140 139
141 140 if request.get? || request.xhr?
142 141 @issue.start_date ||= Date.today
143 142 else
144 143 requested_status = IssueStatus.find_by_id(params[:issue][:status_id])
145 144 # Check that the user is allowed to apply the requested status
146 145 @issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status
147 146 if @issue.save
148 147 attach_files(@issue, params[:attachments])
149 148 flash[:notice] = l(:notice_successful_create)
150 149 Mailer.deliver_issue_add(@issue) if Setting.notified_events.include?('issue_added')
151 150 redirect_to :controller => 'issues', :action => 'show', :id => @issue
152 151 return
153 152 end
154 153 end
155 154 @priorities = Enumeration::get_values('IPRI')
156 155 render :layout => !request.xhr?
157 156 end
158 157
159 158 # Attributes that can be updated on workflow transition (without :edit permission)
160 159 # TODO: make it configurable (at least per role)
161 160 UPDATABLE_ATTRS_ON_TRANSITION = %w(status_id assigned_to_id fixed_version_id done_ratio) unless const_defined?(:UPDATABLE_ATTRS_ON_TRANSITION)
162 161
163 162 def edit
164 163 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
165 164 @priorities = Enumeration::get_values('IPRI')
166 165 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
167 166 @time_entry = TimeEntry.new
168 167
169 168 @notes = params[:notes]
170 169 journal = @issue.init_journal(User.current, @notes)
171 170 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
172 171 if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue]
173 172 attrs = params[:issue].dup
174 173 attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed
175 174 attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s}
176 175 @issue.attributes = attrs
177 176 end
178 177
179 178 if request.post?
180 179 @time_entry = TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
181 180 @time_entry.attributes = params[:time_entry]
182 181 attachments = attach_files(@issue, params[:attachments])
183 182 attachments.each {|a| journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
184 183
185 184 call_hook(:controller_issues_edit_before_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
186 185
187 186 if (@time_entry.hours.nil? || @time_entry.valid?) && @issue.save
188 187 # Log spend time
189 188 if current_role.allowed_to?(:log_time)
190 189 @time_entry.save
191 190 end
192 191 if !journal.new_record?
193 192 # Only send notification if something was actually changed
194 193 flash[:notice] = l(:notice_successful_update)
195 194 Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
196 195 end
197 196 redirect_to(params[:back_to] || {:action => 'show', :id => @issue})
198 197 end
199 198 end
200 199 rescue ActiveRecord::StaleObjectError
201 200 # Optimistic locking exception
202 201 flash.now[:error] = l(:notice_locking_conflict)
203 202 end
204 203
205 204 def reply
206 205 journal = Journal.find(params[:journal_id]) if params[:journal_id]
207 206 if journal
208 207 user = journal.user
209 208 text = journal.notes
210 209 else
211 210 user = @issue.author
212 211 text = @issue.description
213 212 end
214 213 content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
215 214 content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
216 215 render(:update) { |page|
217 216 page.<< "$('notes').value = \"#{content}\";"
218 217 page.show 'update'
219 218 page << "Form.Element.focus('notes');"
220 219 page << "Element.scrollTo('update');"
221 220 page << "$('notes').scrollTop = $('notes').scrollHeight - $('notes').clientHeight;"
222 221 }
223 222 end
224 223
225 224 # Bulk edit a set of issues
226 225 def bulk_edit
227 226 if request.post?
228 227 status = params[:status_id].blank? ? nil : IssueStatus.find_by_id(params[:status_id])
229 228 priority = params[:priority_id].blank? ? nil : Enumeration.find_by_id(params[:priority_id])
230 229 assigned_to = (params[:assigned_to_id].blank? || params[:assigned_to_id] == 'none') ? nil : User.find_by_id(params[:assigned_to_id])
231 230 category = (params[:category_id].blank? || params[:category_id] == 'none') ? nil : @project.issue_categories.find_by_id(params[:category_id])
232 231 fixed_version = (params[:fixed_version_id].blank? || params[:fixed_version_id] == 'none') ? nil : @project.versions.find_by_id(params[:fixed_version_id])
233 232
234 233 unsaved_issue_ids = []
235 234 @issues.each do |issue|
236 235 journal = issue.init_journal(User.current, params[:notes])
237 236 issue.priority = priority if priority
238 237 issue.assigned_to = assigned_to if assigned_to || params[:assigned_to_id] == 'none'
239 238 issue.category = category if category || params[:category_id] == 'none'
240 239 issue.fixed_version = fixed_version if fixed_version || params[:fixed_version_id] == 'none'
241 240 issue.start_date = params[:start_date] unless params[:start_date].blank?
242 241 issue.due_date = params[:due_date] unless params[:due_date].blank?
243 242 issue.done_ratio = params[:done_ratio] unless params[:done_ratio].blank?
244 243 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
245 244 # Don't save any change to the issue if the user is not authorized to apply the requested status
246 245 if (status.nil? || (issue.status.new_status_allowed_to?(status, current_role, issue.tracker) && issue.status = status)) && issue.save
247 246 # Send notification for each issue (if changed)
248 247 Mailer.deliver_issue_edit(journal) if journal.details.any? && Setting.notified_events.include?('issue_updated')
249 248 else
250 249 # Keep unsaved issue ids to display them in flash error
251 250 unsaved_issue_ids << issue.id
252 251 end
253 252 end
254 253 if unsaved_issue_ids.empty?
255 254 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
256 255 else
257 256 flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #'))
258 257 end
259 258 redirect_to(params[:back_to] || {:controller => 'issues', :action => 'index', :project_id => @project})
260 259 return
261 260 end
262 261 # Find potential statuses the user could be allowed to switch issues to
263 262 @available_statuses = Workflow.find(:all, :include => :new_status,
264 263 :conditions => {:role_id => current_role.id}).collect(&:new_status).compact.uniq.sort
265 264 end
266 265
267 266 def move
268 267 @allowed_projects = []
269 268 # find projects to which the user is allowed to move the issue
270 269 if User.current.admin?
271 270 # admin is allowed to move issues to any active (visible) project
272 271 @allowed_projects = Project.find(:all, :conditions => Project.visible_by(User.current), :order => 'name')
273 272 else
274 273 User.current.memberships.each {|m| @allowed_projects << m.project if m.role.allowed_to?(:move_issues)}
275 274 end
276 275 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id]
277 276 @target_project ||= @project
278 277 @trackers = @target_project.trackers
279 278 if request.post?
280 279 new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id])
281 280 unsaved_issue_ids = []
282 281 @issues.each do |issue|
283 282 issue.init_journal(User.current)
284 283 unsaved_issue_ids << issue.id unless issue.move_to(@target_project, new_tracker)
285 284 end
286 285 if unsaved_issue_ids.empty?
287 286 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
288 287 else
289 288 flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #'))
290 289 end
291 290 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
292 291 return
293 292 end
294 293 render :layout => false if request.xhr?
295 294 end
296 295
297 296 def destroy
298 297 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
299 298 if @hours > 0
300 299 case params[:todo]
301 300 when 'destroy'
302 301 # nothing to do
303 302 when 'nullify'
304 303 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
305 304 when 'reassign'
306 305 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
307 306 if reassign_to.nil?
308 307 flash.now[:error] = l(:error_issue_not_found_in_project)
309 308 return
310 309 else
311 310 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
312 311 end
313 312 else
314 313 # display the destroy form
315 314 return
316 315 end
317 316 end
318 317 @issues.each(&:destroy)
319 318 redirect_to :action => 'index', :project_id => @project
320 319 end
321 320
322 321 def gantt
323 322 @gantt = Redmine::Helpers::Gantt.new(params)
324 323 retrieve_query
325 324 if @query.valid?
326 325 events = []
327 326 # Issues that have start and due dates
328 327 events += Issue.find(:all,
329 328 :order => "start_date, due_date",
330 329 :include => [:tracker, :status, :assigned_to, :priority, :project],
331 330 :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]
332 331 )
333 332 # Issues that don't have a due date but that are assigned to a version with a date
334 333 events += Issue.find(:all,
335 334 :order => "start_date, effective_date",
336 335 :include => [:tracker, :status, :assigned_to, :priority, :project, :fixed_version],
337 336 :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]
338 337 )
339 338 # Versions
340 339 events += Version.find(:all, :include => :project,
341 340 :conditions => ["(#{@query.project_statement}) AND effective_date BETWEEN ? AND ?", @gantt.date_from, @gantt.date_to])
342 341
343 342 @gantt.events = events
344 343 end
345 344
346 345 respond_to do |format|
347 346 format.html { render :template => "issues/gantt.rhtml", :layout => !request.xhr? }
348 347 format.png { send_data(@gantt.to_image, :disposition => 'inline', :type => 'image/png', :filename => "#{@project.identifier}-gantt.png") } if @gantt.respond_to?('to_image')
349 format.pdf { send_data(render(:template => "issues/gantt.rfpdf", :layout => false), :type => 'application/pdf', :filename => "#{@project.nil? ? '' : "#{@project.identifier}-" }gantt.pdf") }
348 format.pdf { send_data(gantt_to_pdf(@gantt, @project), :type => 'application/pdf', :filename => "#{@project.nil? ? '' : "#{@project.identifier}-" }gantt.pdf") }
350 349 end
351 350 end
352 351
353 352 def calendar
354 353 if params[:year] and params[:year].to_i > 1900
355 354 @year = params[:year].to_i
356 355 if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
357 356 @month = params[:month].to_i
358 357 end
359 358 end
360 359 @year ||= Date.today.year
361 360 @month ||= Date.today.month
362 361
363 362 @calendar = Redmine::Helpers::Calendar.new(Date.civil(@year, @month, 1), current_language, :month)
364 363 retrieve_query
365 364 if @query.valid?
366 365 events = []
367 366 events += Issue.find(:all,
368 367 :include => [:tracker, :status, :assigned_to, :priority, :project],
369 368 :conditions => ["(#{@query.statement}) AND ((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?))", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt]
370 369 )
371 370 events += Version.find(:all, :include => :project,
372 371 :conditions => ["(#{@query.project_statement}) AND effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt])
373 372
374 373 @calendar.events = events
375 374 end
376 375
377 376 render :layout => false if request.xhr?
378 377 end
379 378
380 379 def context_menu
381 380 @issues = Issue.find_all_by_id(params[:ids], :include => :project)
382 381 if (@issues.size == 1)
383 382 @issue = @issues.first
384 383 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
385 384 end
386 385 projects = @issues.collect(&:project).compact.uniq
387 386 @project = projects.first if projects.size == 1
388 387
389 388 @can = {:edit => (@project && User.current.allowed_to?(:edit_issues, @project)),
390 389 :log_time => (@project && User.current.allowed_to?(:log_time, @project)),
391 390 :update => (@project && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && @allowed_statuses && !@allowed_statuses.empty?))),
392 391 :move => (@project && User.current.allowed_to?(:move_issues, @project)),
393 392 :copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
394 393 :delete => (@project && User.current.allowed_to?(:delete_issues, @project))
395 394 }
396 395 if @project
397 396 @assignables = @project.assignable_users
398 397 @assignables << @issue.assigned_to if @issue && @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
399 398 end
400 399
401 400 @priorities = Enumeration.get_values('IPRI').reverse
402 401 @statuses = IssueStatus.find(:all, :order => 'position')
403 402 @back = request.env['HTTP_REFERER']
404 403
405 404 render :layout => false
406 405 end
407 406
408 407 def update_form
409 408 @issue = Issue.new(params[:issue])
410 409 render :action => :new, :layout => false
411 410 end
412 411
413 412 def preview
414 413 @issue = @project.issues.find_by_id(params[:id]) unless params[:id].blank?
415 414 @attachements = @issue.attachments if @issue
416 415 @text = params[:notes] || (params[:issue] ? params[:issue][:description] : nil)
417 416 render :partial => 'common/preview'
418 417 end
419 418
420 419 private
421 420 def find_issue
422 421 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
423 422 @project = @issue.project
424 423 rescue ActiveRecord::RecordNotFound
425 424 render_404
426 425 end
427 426
428 427 # Filter for bulk operations
429 428 def find_issues
430 429 @issues = Issue.find_all_by_id(params[:id] || params[:ids])
431 430 raise ActiveRecord::RecordNotFound if @issues.empty?
432 431 projects = @issues.collect(&:project).compact.uniq
433 432 if projects.size == 1
434 433 @project = projects.first
435 434 else
436 435 # TODO: let users bulk edit/move/destroy issues from different projects
437 436 render_error 'Can not bulk edit/move/destroy issues from different projects' and return false
438 437 end
439 438 rescue ActiveRecord::RecordNotFound
440 439 render_404
441 440 end
442 441
443 442 def find_project
444 443 @project = Project.find(params[:project_id])
445 444 rescue ActiveRecord::RecordNotFound
446 445 render_404
447 446 end
448 447
449 448 def find_optional_project
450 449 @project = Project.find(params[:project_id]) unless params[:project_id].blank?
451 450 allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
452 451 allowed ? true : deny_access
453 452 rescue ActiveRecord::RecordNotFound
454 453 render_404
455 454 end
456 455
457 456 # Retrieve query from session or build a new query
458 457 def retrieve_query
459 458 if !params[:query_id].blank?
460 459 cond = "project_id IS NULL"
461 460 cond << " OR project_id = #{@project.id}" if @project
462 461 @query = Query.find(params[:query_id], :conditions => cond)
463 462 @query.project = @project
464 463 session[:query] = {:id => @query.id, :project_id => @query.project_id}
465 464 else
466 465 if params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
467 466 # Give it a name, required to be valid
468 467 @query = Query.new(:name => "_")
469 468 @query.project = @project
470 469 if params[:fields] and params[:fields].is_a? Array
471 470 params[:fields].each do |field|
472 471 @query.add_filter(field, params[:operators][field], params[:values][field])
473 472 end
474 473 else
475 474 @query.available_filters.keys.each do |field|
476 475 @query.add_short_filter(field, params[field]) if params[field]
477 476 end
478 477 end
479 478 session[:query] = {:project_id => @query.project_id, :filters => @query.filters}
480 479 else
481 480 @query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
482 481 @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters])
483 482 @query.project = @project
484 483 end
485 484 end
486 485 end
487 486 end
@@ -1,298 +1,296
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 class ProjectsController < ApplicationController
19 19 menu_item :overview
20 20 menu_item :activity, :only => :activity
21 21 menu_item :roadmap, :only => :roadmap
22 22 menu_item :files, :only => [:list_files, :add_file]
23 23 menu_item :settings, :only => :settings
24 24 menu_item :issues, :only => [:changelog]
25 25
26 26 before_filter :find_project, :except => [ :index, :list, :add, :activity ]
27 27 before_filter :find_optional_project, :only => :activity
28 28 before_filter :authorize, :except => [ :index, :list, :add, :archive, :unarchive, :destroy, :activity ]
29 29 before_filter :require_admin, :only => [ :add, :archive, :unarchive, :destroy ]
30 30 accept_key_auth :activity
31 31
32 32 helper :sort
33 33 include SortHelper
34 34 helper :custom_fields
35 35 include CustomFieldsHelper
36 helper :ifpdf
37 include IfpdfHelper
38 36 helper :issues
39 37 helper IssuesHelper
40 38 helper :queries
41 39 include QueriesHelper
42 40 helper :repositories
43 41 include RepositoriesHelper
44 42 include ProjectsHelper
45 43
46 44 # Lists visible projects
47 45 def index
48 46 projects = Project.find :all,
49 47 :conditions => Project.visible_by(User.current),
50 48 :include => :parent
51 49 respond_to do |format|
52 50 format.html {
53 51 @project_tree = projects.group_by {|p| p.parent || p}
54 52 @project_tree.keys.each {|p| @project_tree[p] -= [p]}
55 53 }
56 54 format.atom {
57 55 render_feed(projects.sort_by(&:created_on).reverse.slice(0, Setting.feeds_limit.to_i),
58 56 :title => "#{Setting.app_title}: #{l(:label_project_latest)}")
59 57 }
60 58 end
61 59 end
62 60
63 61 # Add a new project
64 62 def add
65 63 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
66 64 @trackers = Tracker.all
67 65 @root_projects = Project.find(:all,
68 66 :conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}",
69 67 :order => 'name')
70 68 @project = Project.new(params[:project])
71 69 if request.get?
72 70 @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
73 71 @project.trackers = Tracker.all
74 72 @project.is_public = Setting.default_projects_public?
75 73 @project.enabled_module_names = Redmine::AccessControl.available_project_modules
76 74 else
77 75 @project.enabled_module_names = params[:enabled_modules]
78 76 if @project.save
79 77 flash[:notice] = l(:notice_successful_create)
80 78 redirect_to :controller => 'admin', :action => 'projects'
81 79 end
82 80 end
83 81 end
84 82
85 83 # Show @project
86 84 def show
87 85 if params[:jump]
88 86 # try to redirect to the requested menu item
89 87 redirect_to_project_menu_item(@project, params[:jump]) && return
90 88 end
91 89
92 90 @members_by_role = @project.members.find(:all, :include => [:user, :role], :order => 'position').group_by {|m| m.role}
93 91 @subprojects = @project.children.find(:all, :conditions => Project.visible_by(User.current))
94 92 @news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
95 93 @trackers = @project.rolled_up_trackers
96 94
97 95 cond = @project.project_condition(Setting.display_subprojects_issues?)
98 96 Issue.visible_by(User.current) do
99 97 @open_issues_by_tracker = Issue.count(:group => :tracker,
100 98 :include => [:project, :status, :tracker],
101 99 :conditions => ["(#{cond}) AND #{IssueStatus.table_name}.is_closed=?", false])
102 100 @total_issues_by_tracker = Issue.count(:group => :tracker,
103 101 :include => [:project, :status, :tracker],
104 102 :conditions => cond)
105 103 end
106 104 TimeEntry.visible_by(User.current) do
107 105 @total_hours = TimeEntry.sum(:hours,
108 106 :include => :project,
109 107 :conditions => cond).to_f
110 108 end
111 109 @key = User.current.rss_key
112 110 end
113 111
114 112 def settings
115 113 @root_projects = Project.find(:all,
116 114 :conditions => ["parent_id IS NULL AND status = #{Project::STATUS_ACTIVE} AND id <> ?", @project.id],
117 115 :order => 'name')
118 116 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
119 117 @issue_category ||= IssueCategory.new
120 118 @member ||= @project.members.new
121 119 @trackers = Tracker.all
122 120 @repository ||= @project.repository
123 121 @wiki ||= @project.wiki
124 122 end
125 123
126 124 # Edit @project
127 125 def edit
128 126 if request.post?
129 127 @project.attributes = params[:project]
130 128 if @project.save
131 129 flash[:notice] = l(:notice_successful_update)
132 130 redirect_to :action => 'settings', :id => @project
133 131 else
134 132 settings
135 133 render :action => 'settings'
136 134 end
137 135 end
138 136 end
139 137
140 138 def modules
141 139 @project.enabled_module_names = params[:enabled_modules]
142 140 redirect_to :action => 'settings', :id => @project, :tab => 'modules'
143 141 end
144 142
145 143 def archive
146 144 @project.archive if request.post? && @project.active?
147 145 redirect_to :controller => 'admin', :action => 'projects'
148 146 end
149 147
150 148 def unarchive
151 149 @project.unarchive if request.post? && !@project.active?
152 150 redirect_to :controller => 'admin', :action => 'projects'
153 151 end
154 152
155 153 # Delete @project
156 154 def destroy
157 155 @project_to_destroy = @project
158 156 if request.post? and params[:confirm]
159 157 @project_to_destroy.destroy
160 158 redirect_to :controller => 'admin', :action => 'projects'
161 159 end
162 160 # hide project in layout
163 161 @project = nil
164 162 end
165 163
166 164 # Add a new issue category to @project
167 165 def add_issue_category
168 166 @category = @project.issue_categories.build(params[:category])
169 167 if request.post? and @category.save
170 168 respond_to do |format|
171 169 format.html do
172 170 flash[:notice] = l(:notice_successful_create)
173 171 redirect_to :action => 'settings', :tab => 'categories', :id => @project
174 172 end
175 173 format.js do
176 174 # IE doesn't support the replace_html rjs method for select box options
177 175 render(:update) {|page| page.replace "issue_category_id",
178 176 content_tag('select', '<option></option>' + options_from_collection_for_select(@project.issue_categories, 'id', 'name', @category.id), :id => 'issue_category_id', :name => 'issue[category_id]')
179 177 }
180 178 end
181 179 end
182 180 end
183 181 end
184 182
185 183 # Add a new version to @project
186 184 def add_version
187 185 @version = @project.versions.build(params[:version])
188 186 if request.post? and @version.save
189 187 flash[:notice] = l(:notice_successful_create)
190 188 redirect_to :action => 'settings', :tab => 'versions', :id => @project
191 189 end
192 190 end
193 191
194 192 def add_file
195 193 if request.post?
196 194 container = (params[:version_id].blank? ? @project : @project.versions.find_by_id(params[:version_id]))
197 195 attachments = attach_files(container, params[:attachments])
198 196 if !attachments.empty? && Setting.notified_events.include?('file_added')
199 197 Mailer.deliver_attachments_added(attachments)
200 198 end
201 199 redirect_to :controller => 'projects', :action => 'list_files', :id => @project
202 200 return
203 201 end
204 202 @versions = @project.versions.sort
205 203 end
206 204
207 205 def list_files
208 206 sort_init 'filename', 'asc'
209 207 sort_update 'filename' => "#{Attachment.table_name}.filename",
210 208 'created_on' => "#{Attachment.table_name}.created_on",
211 209 'size' => "#{Attachment.table_name}.filesize",
212 210 'downloads' => "#{Attachment.table_name}.downloads"
213 211
214 212 @containers = [ Project.find(@project.id, :include => :attachments, :order => sort_clause)]
215 213 @containers += @project.versions.find(:all, :include => :attachments, :order => sort_clause).sort.reverse
216 214 render :layout => !request.xhr?
217 215 end
218 216
219 217 # Show changelog for @project
220 218 def changelog
221 219 @trackers = @project.trackers.find(:all, :conditions => ["is_in_chlog=?", true], :order => 'position')
222 220 retrieve_selected_tracker_ids(@trackers)
223 221 @versions = @project.versions.sort
224 222 end
225 223
226 224 def roadmap
227 225 @trackers = @project.trackers.find(:all, :conditions => ["is_in_roadmap=?", true])
228 226 retrieve_selected_tracker_ids(@trackers)
229 227 @versions = @project.versions.sort
230 228 @versions = @versions.select {|v| !v.completed? } unless params[:completed]
231 229 end
232 230
233 231 def activity
234 232 @days = Setting.activity_days_default.to_i
235 233
236 234 if params[:from]
237 235 begin; @date_to = params[:from].to_date + 1; rescue; end
238 236 end
239 237
240 238 @date_to ||= Date.today + 1
241 239 @date_from = @date_to - @days
242 240 @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
243 241 @author = (params[:user_id].blank? ? nil : User.active.find(params[:user_id]))
244 242
245 243 @activity = Redmine::Activity::Fetcher.new(User.current, :project => @project,
246 244 :with_subprojects => @with_subprojects,
247 245 :author => @author)
248 246 @activity.scope_select {|t| !params["show_#{t}"].nil?}
249 247 @activity.scope = (@author.nil? ? :default : :all) if @activity.scope.empty?
250 248
251 249 events = @activity.events(@date_from, @date_to)
252 250
253 251 respond_to do |format|
254 252 format.html {
255 253 @events_by_day = events.group_by(&:event_date)
256 254 render :layout => false if request.xhr?
257 255 }
258 256 format.atom {
259 257 title = l(:label_activity)
260 258 if @author
261 259 title = @author.name
262 260 elsif @activity.scope.size == 1
263 261 title = l("label_#{@activity.scope.first.singularize}_plural")
264 262 end
265 263 render_feed(events, :title => "#{@project || Setting.app_title}: #{title}")
266 264 }
267 265 end
268 266
269 267 rescue ActiveRecord::RecordNotFound
270 268 render_404
271 269 end
272 270
273 271 private
274 272 # Find project of id params[:id]
275 273 # if not found, redirect to project list
276 274 # Used as a before_filter
277 275 def find_project
278 276 @project = Project.find(params[:id])
279 277 rescue ActiveRecord::RecordNotFound
280 278 render_404
281 279 end
282 280
283 281 def find_optional_project
284 282 return true unless params[:id]
285 283 @project = Project.find(params[:id])
286 284 authorize
287 285 rescue ActiveRecord::RecordNotFound
288 286 render_404
289 287 end
290 288
291 289 def retrieve_selected_tracker_ids(selectable_trackers)
292 290 if ids = params[:tracker_ids]
293 291 @selected_tracker_ids = (ids.is_a? Array) ? ids.collect { |id| id.to_i.to_s } : ids.split('/').collect { |id| id.to_i.to_s }
294 292 else
295 293 @selected_tracker_ids = selectable_trackers.collect {|t| t.id.to_s }
296 294 end
297 295 end
298 296 end
@@ -1,750 +1,758
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_index_sort
142 142 get :index, :sort_key => 'tracker'
143 143 assert_response :success
144 144
145 145 sort_params = @request.session['issuesindex_sort']
146 146 assert sort_params.is_a?(Hash)
147 147 assert_equal 'tracker', sort_params[:key]
148 148 assert_equal 'ASC', sort_params[:order]
149 149 end
150 150
151 151 def test_gantt
152 152 get :gantt, :project_id => 1
153 153 assert_response :success
154 154 assert_template 'gantt.rhtml'
155 155 assert_not_nil assigns(:gantt)
156 156 events = assigns(:gantt).events
157 157 assert_not_nil events
158 158 # Issue with start and due dates
159 159 i = Issue.find(1)
160 160 assert_not_nil i.due_date
161 161 assert events.include?(Issue.find(1))
162 162 # Issue with without due date but targeted to a version with date
163 163 i = Issue.find(2)
164 164 assert_nil i.due_date
165 165 assert events.include?(i)
166 166 end
167 167
168 168 def test_cross_project_gantt
169 169 get :gantt
170 170 assert_response :success
171 171 assert_template 'gantt.rhtml'
172 172 assert_not_nil assigns(:gantt)
173 173 events = assigns(:gantt).events
174 174 assert_not_nil events
175 175 end
176 176
177 177 def test_gantt_export_to_pdf
178 178 get :gantt, :project_id => 1, :format => 'pdf'
179 179 assert_response :success
180 assert_template 'gantt.rfpdf'
181 180 assert_equal 'application/pdf', @response.content_type
181 assert @response.body.starts_with?('%PDF')
182 182 assert_not_nil assigns(:gantt)
183 183 end
184 184
185 185 def test_cross_project_gantt_export_to_pdf
186 186 get :gantt, :format => 'pdf'
187 187 assert_response :success
188 assert_template 'gantt.rfpdf'
189 188 assert_equal 'application/pdf', @response.content_type
189 assert @response.body.starts_with?('%PDF')
190 190 assert_not_nil assigns(:gantt)
191 191 end
192 192
193 193 if Object.const_defined?(:Magick)
194 194 def test_gantt_image
195 195 get :gantt, :project_id => 1, :format => 'png'
196 196 assert_response :success
197 197 assert_equal 'image/png', @response.content_type
198 198 end
199 199 else
200 200 puts "RMagick not installed. Skipping tests !!!"
201 201 end
202 202
203 203 def test_calendar
204 204 get :calendar, :project_id => 1
205 205 assert_response :success
206 206 assert_template 'calendar'
207 207 assert_not_nil assigns(:calendar)
208 208 end
209 209
210 210 def test_cross_project_calendar
211 211 get :calendar
212 212 assert_response :success
213 213 assert_template 'calendar'
214 214 assert_not_nil assigns(:calendar)
215 215 end
216 216
217 217 def test_changes
218 218 get :changes, :project_id => 1
219 219 assert_response :success
220 220 assert_not_nil assigns(:journals)
221 221 assert_equal 'application/atom+xml', @response.content_type
222 222 end
223 223
224 224 def test_show_by_anonymous
225 225 get :show, :id => 1
226 226 assert_response :success
227 227 assert_template 'show.rhtml'
228 228 assert_not_nil assigns(:issue)
229 229 assert_equal Issue.find(1), assigns(:issue)
230 230
231 231 # anonymous role is allowed to add a note
232 232 assert_tag :tag => 'form',
233 233 :descendant => { :tag => 'fieldset',
234 234 :child => { :tag => 'legend',
235 235 :content => /Notes/ } }
236 236 end
237 237
238 238 def test_show_by_manager
239 239 @request.session[:user_id] = 2
240 240 get :show, :id => 1
241 241 assert_response :success
242 242
243 243 assert_tag :tag => 'form',
244 244 :descendant => { :tag => 'fieldset',
245 245 :child => { :tag => 'legend',
246 246 :content => /Change properties/ } },
247 247 :descendant => { :tag => 'fieldset',
248 248 :child => { :tag => 'legend',
249 249 :content => /Log time/ } },
250 250 :descendant => { :tag => 'fieldset',
251 251 :child => { :tag => 'legend',
252 252 :content => /Notes/ } }
253 253 end
254 254
255 def test_show_export_to_pdf
256 get :show, :id => 1, :format => 'pdf'
257 assert_response :success
258 assert_equal 'application/pdf', @response.content_type
259 assert @response.body.starts_with?('%PDF')
260 assert_not_nil assigns(:issue)
261 end
262
255 263 def test_get_new
256 264 @request.session[:user_id] = 2
257 265 get :new, :project_id => 1, :tracker_id => 1
258 266 assert_response :success
259 267 assert_template 'new'
260 268
261 269 assert_tag :tag => 'input', :attributes => { :name => 'issue[custom_field_values][2]',
262 270 :value => 'Default string' }
263 271 end
264 272
265 273 def test_get_new_without_tracker_id
266 274 @request.session[:user_id] = 2
267 275 get :new, :project_id => 1
268 276 assert_response :success
269 277 assert_template 'new'
270 278
271 279 issue = assigns(:issue)
272 280 assert_not_nil issue
273 281 assert_equal Project.find(1).trackers.first, issue.tracker
274 282 end
275 283
276 284 def test_update_new_form
277 285 @request.session[:user_id] = 2
278 286 xhr :post, :new, :project_id => 1,
279 287 :issue => {:tracker_id => 2,
280 288 :subject => 'This is the test_new issue',
281 289 :description => 'This is the description',
282 290 :priority_id => 5}
283 291 assert_response :success
284 292 assert_template 'new'
285 293 end
286 294
287 295 def test_post_new
288 296 @request.session[:user_id] = 2
289 297 post :new, :project_id => 1,
290 298 :issue => {:tracker_id => 3,
291 299 :subject => 'This is the test_new issue',
292 300 :description => 'This is the description',
293 301 :priority_id => 5,
294 302 :estimated_hours => '',
295 303 :custom_field_values => {'2' => 'Value for field 2'}}
296 304 assert_redirected_to 'issues/show'
297 305
298 306 issue = Issue.find_by_subject('This is the test_new issue')
299 307 assert_not_nil issue
300 308 assert_equal 2, issue.author_id
301 309 assert_equal 3, issue.tracker_id
302 310 assert_nil issue.estimated_hours
303 311 v = issue.custom_values.find(:first, :conditions => {:custom_field_id => 2})
304 312 assert_not_nil v
305 313 assert_equal 'Value for field 2', v.value
306 314 end
307 315
308 316 def test_post_new_without_custom_fields_param
309 317 @request.session[:user_id] = 2
310 318 post :new, :project_id => 1,
311 319 :issue => {:tracker_id => 1,
312 320 :subject => 'This is the test_new issue',
313 321 :description => 'This is the description',
314 322 :priority_id => 5}
315 323 assert_redirected_to 'issues/show'
316 324 end
317 325
318 326 def test_post_new_with_required_custom_field_and_without_custom_fields_param
319 327 field = IssueCustomField.find_by_name('Database')
320 328 field.update_attribute(:is_required, true)
321 329
322 330 @request.session[:user_id] = 2
323 331 post :new, :project_id => 1,
324 332 :issue => {:tracker_id => 1,
325 333 :subject => 'This is the test_new issue',
326 334 :description => 'This is the description',
327 335 :priority_id => 5}
328 336 assert_response :success
329 337 assert_template 'new'
330 338 issue = assigns(:issue)
331 339 assert_not_nil issue
332 340 assert_equal 'activerecord_error_invalid', issue.errors.on(:custom_values)
333 341 end
334 342
335 343 def test_post_new_with_watchers
336 344 @request.session[:user_id] = 2
337 345 ActionMailer::Base.deliveries.clear
338 346
339 347 assert_difference 'Watcher.count', 2 do
340 348 post :new, :project_id => 1,
341 349 :issue => {:tracker_id => 1,
342 350 :subject => 'This is a new issue with watchers',
343 351 :description => 'This is the description',
344 352 :priority_id => 5,
345 353 :watcher_user_ids => ['2', '3']}
346 354 end
347 355 assert_redirected_to 'issues/show'
348 356
349 357 issue = Issue.find_by_subject('This is a new issue with watchers')
350 358 # Watchers added
351 359 assert_equal [2, 3], issue.watcher_user_ids.sort
352 360 assert issue.watched_by?(User.find(3))
353 361 # Watchers notified
354 362 mail = ActionMailer::Base.deliveries.last
355 363 assert_kind_of TMail::Mail, mail
356 364 assert [mail.bcc, mail.cc].flatten.include?(User.find(3).mail)
357 365 end
358 366
359 367 def test_post_should_preserve_fields_values_on_validation_failure
360 368 @request.session[:user_id] = 2
361 369 post :new, :project_id => 1,
362 370 :issue => {:tracker_id => 1,
363 371 :subject => 'This is the test_new issue',
364 372 # empty description
365 373 :description => '',
366 374 :priority_id => 6,
367 375 :custom_field_values => {'1' => 'Oracle', '2' => 'Value for field 2'}}
368 376 assert_response :success
369 377 assert_template 'new'
370 378
371 379 assert_tag :input, :attributes => { :name => 'issue[subject]',
372 380 :value => 'This is the test_new issue' }
373 381 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
374 382 :child => { :tag => 'option', :attributes => { :selected => 'selected',
375 383 :value => '6' },
376 384 :content => 'High' }
377 385 # Custom fields
378 386 assert_tag :select, :attributes => { :name => 'issue[custom_field_values][1]' },
379 387 :child => { :tag => 'option', :attributes => { :selected => 'selected',
380 388 :value => 'Oracle' },
381 389 :content => 'Oracle' }
382 390 assert_tag :input, :attributes => { :name => 'issue[custom_field_values][2]',
383 391 :value => 'Value for field 2'}
384 392 end
385 393
386 394 def test_copy_issue
387 395 @request.session[:user_id] = 2
388 396 get :new, :project_id => 1, :copy_from => 1
389 397 assert_template 'new'
390 398 assert_not_nil assigns(:issue)
391 399 orig = Issue.find(1)
392 400 assert_equal orig.subject, assigns(:issue).subject
393 401 end
394 402
395 403 def test_get_edit
396 404 @request.session[:user_id] = 2
397 405 get :edit, :id => 1
398 406 assert_response :success
399 407 assert_template 'edit'
400 408 assert_not_nil assigns(:issue)
401 409 assert_equal Issue.find(1), assigns(:issue)
402 410 end
403 411
404 412 def test_get_edit_with_params
405 413 @request.session[:user_id] = 2
406 414 get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 }
407 415 assert_response :success
408 416 assert_template 'edit'
409 417
410 418 issue = assigns(:issue)
411 419 assert_not_nil issue
412 420
413 421 assert_equal 5, issue.status_id
414 422 assert_tag :select, :attributes => { :name => 'issue[status_id]' },
415 423 :child => { :tag => 'option',
416 424 :content => 'Closed',
417 425 :attributes => { :selected => 'selected' } }
418 426
419 427 assert_equal 7, issue.priority_id
420 428 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
421 429 :child => { :tag => 'option',
422 430 :content => 'Urgent',
423 431 :attributes => { :selected => 'selected' } }
424 432 end
425 433
426 434 def test_reply_to_issue
427 435 @request.session[:user_id] = 2
428 436 get :reply, :id => 1
429 437 assert_response :success
430 438 assert_select_rjs :show, "update"
431 439 end
432 440
433 441 def test_reply_to_note
434 442 @request.session[:user_id] = 2
435 443 get :reply, :id => 1, :journal_id => 2
436 444 assert_response :success
437 445 assert_select_rjs :show, "update"
438 446 end
439 447
440 448 def test_post_edit_without_custom_fields_param
441 449 @request.session[:user_id] = 2
442 450 ActionMailer::Base.deliveries.clear
443 451
444 452 issue = Issue.find(1)
445 453 assert_equal '125', issue.custom_value_for(2).value
446 454 old_subject = issue.subject
447 455 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
448 456
449 457 assert_difference('Journal.count') do
450 458 assert_difference('JournalDetail.count', 2) do
451 459 post :edit, :id => 1, :issue => {:subject => new_subject,
452 460 :priority_id => '6',
453 461 :category_id => '1' # no change
454 462 }
455 463 end
456 464 end
457 465 assert_redirected_to 'issues/show/1'
458 466 issue.reload
459 467 assert_equal new_subject, issue.subject
460 468 # Make sure custom fields were not cleared
461 469 assert_equal '125', issue.custom_value_for(2).value
462 470
463 471 mail = ActionMailer::Base.deliveries.last
464 472 assert_kind_of TMail::Mail, mail
465 473 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
466 474 assert mail.body.include?("Subject changed from #{old_subject} to #{new_subject}")
467 475 end
468 476
469 477 def test_post_edit_with_custom_field_change
470 478 @request.session[:user_id] = 2
471 479 issue = Issue.find(1)
472 480 assert_equal '125', issue.custom_value_for(2).value
473 481
474 482 assert_difference('Journal.count') do
475 483 assert_difference('JournalDetail.count', 3) do
476 484 post :edit, :id => 1, :issue => {:subject => 'Custom field change',
477 485 :priority_id => '6',
478 486 :category_id => '1', # no change
479 487 :custom_field_values => { '2' => 'New custom value' }
480 488 }
481 489 end
482 490 end
483 491 assert_redirected_to 'issues/show/1'
484 492 issue.reload
485 493 assert_equal 'New custom value', issue.custom_value_for(2).value
486 494
487 495 mail = ActionMailer::Base.deliveries.last
488 496 assert_kind_of TMail::Mail, mail
489 497 assert mail.body.include?("Searchable field changed from 125 to New custom value")
490 498 end
491 499
492 500 def test_post_edit_with_status_and_assignee_change
493 501 issue = Issue.find(1)
494 502 assert_equal 1, issue.status_id
495 503 @request.session[:user_id] = 2
496 504 assert_difference('TimeEntry.count', 0) do
497 505 post :edit,
498 506 :id => 1,
499 507 :issue => { :status_id => 2, :assigned_to_id => 3 },
500 508 :notes => 'Assigned to dlopper',
501 509 :time_entry => { :hours => '', :comments => '', :activity_id => Enumeration.get_values('ACTI').first }
502 510 end
503 511 assert_redirected_to 'issues/show/1'
504 512 issue.reload
505 513 assert_equal 2, issue.status_id
506 514 j = issue.journals.find(:first, :order => 'id DESC')
507 515 assert_equal 'Assigned to dlopper', j.notes
508 516 assert_equal 2, j.details.size
509 517
510 518 mail = ActionMailer::Base.deliveries.last
511 519 assert mail.body.include?("Status changed from New to Assigned")
512 520 end
513 521
514 522 def test_post_edit_with_note_only
515 523 notes = 'Note added by IssuesControllerTest#test_update_with_note_only'
516 524 # anonymous user
517 525 post :edit,
518 526 :id => 1,
519 527 :notes => notes
520 528 assert_redirected_to 'issues/show/1'
521 529 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
522 530 assert_equal notes, j.notes
523 531 assert_equal 0, j.details.size
524 532 assert_equal User.anonymous, j.user
525 533
526 534 mail = ActionMailer::Base.deliveries.last
527 535 assert mail.body.include?(notes)
528 536 end
529 537
530 538 def test_post_edit_with_note_and_spent_time
531 539 @request.session[:user_id] = 2
532 540 spent_hours_before = Issue.find(1).spent_hours
533 541 assert_difference('TimeEntry.count') do
534 542 post :edit,
535 543 :id => 1,
536 544 :notes => '2.5 hours added',
537 545 :time_entry => { :hours => '2.5', :comments => '', :activity_id => Enumeration.get_values('ACTI').first }
538 546 end
539 547 assert_redirected_to 'issues/show/1'
540 548
541 549 issue = Issue.find(1)
542 550
543 551 j = issue.journals.find(:first, :order => 'id DESC')
544 552 assert_equal '2.5 hours added', j.notes
545 553 assert_equal 0, j.details.size
546 554
547 555 t = issue.time_entries.find(:first, :order => 'id DESC')
548 556 assert_not_nil t
549 557 assert_equal 2.5, t.hours
550 558 assert_equal spent_hours_before + 2.5, issue.spent_hours
551 559 end
552 560
553 561 def test_post_edit_with_attachment_only
554 562 set_tmp_attachments_directory
555 563
556 564 # Delete all fixtured journals, a race condition can occur causing the wrong
557 565 # journal to get fetched in the next find.
558 566 Journal.delete_all
559 567
560 568 # anonymous user
561 569 post :edit,
562 570 :id => 1,
563 571 :notes => '',
564 572 :attachments => {'1' => {'file' => test_uploaded_file('testfile.txt', 'text/plain')}}
565 573 assert_redirected_to 'issues/show/1'
566 574 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
567 575 assert j.notes.blank?
568 576 assert_equal 1, j.details.size
569 577 assert_equal 'testfile.txt', j.details.first.value
570 578 assert_equal User.anonymous, j.user
571 579
572 580 mail = ActionMailer::Base.deliveries.last
573 581 assert mail.body.include?('testfile.txt')
574 582 end
575 583
576 584 def test_post_edit_with_no_change
577 585 issue = Issue.find(1)
578 586 issue.journals.clear
579 587 ActionMailer::Base.deliveries.clear
580 588
581 589 post :edit,
582 590 :id => 1,
583 591 :notes => ''
584 592 assert_redirected_to 'issues/show/1'
585 593
586 594 issue.reload
587 595 assert issue.journals.empty?
588 596 # No email should be sent
589 597 assert ActionMailer::Base.deliveries.empty?
590 598 end
591 599
592 600 def test_bulk_edit
593 601 @request.session[:user_id] = 2
594 602 # update issues priority
595 603 post :bulk_edit, :ids => [1, 2], :priority_id => 7, :notes => 'Bulk editing', :assigned_to_id => ''
596 604 assert_response 302
597 605 # check that the issues were updated
598 606 assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id}
599 607 assert_equal 'Bulk editing', Issue.find(1).journals.find(:first, :order => 'created_on DESC').notes
600 608 end
601 609
602 610 def test_bulk_unassign
603 611 assert_not_nil Issue.find(2).assigned_to
604 612 @request.session[:user_id] = 2
605 613 # unassign issues
606 614 post :bulk_edit, :ids => [1, 2], :notes => 'Bulk unassigning', :assigned_to_id => 'none'
607 615 assert_response 302
608 616 # check that the issues were updated
609 617 assert_nil Issue.find(2).assigned_to
610 618 end
611 619
612 620 def test_move_one_issue_to_another_project
613 621 @request.session[:user_id] = 1
614 622 post :move, :id => 1, :new_project_id => 2
615 623 assert_redirected_to 'projects/ecookbook/issues'
616 624 assert_equal 2, Issue.find(1).project_id
617 625 end
618 626
619 627 def test_bulk_move_to_another_project
620 628 @request.session[:user_id] = 1
621 629 post :move, :ids => [1, 2], :new_project_id => 2
622 630 assert_redirected_to 'projects/ecookbook/issues'
623 631 # Issues moved to project 2
624 632 assert_equal 2, Issue.find(1).project_id
625 633 assert_equal 2, Issue.find(2).project_id
626 634 # No tracker change
627 635 assert_equal 1, Issue.find(1).tracker_id
628 636 assert_equal 2, Issue.find(2).tracker_id
629 637 end
630 638
631 639 def test_bulk_move_to_another_tracker
632 640 @request.session[:user_id] = 1
633 641 post :move, :ids => [1, 2], :new_tracker_id => 2
634 642 assert_redirected_to 'projects/ecookbook/issues'
635 643 assert_equal 2, Issue.find(1).tracker_id
636 644 assert_equal 2, Issue.find(2).tracker_id
637 645 end
638 646
639 647 def test_context_menu_one_issue
640 648 @request.session[:user_id] = 2
641 649 get :context_menu, :ids => [1]
642 650 assert_response :success
643 651 assert_template 'context_menu'
644 652 assert_tag :tag => 'a', :content => 'Edit',
645 653 :attributes => { :href => '/issues/edit/1',
646 654 :class => 'icon-edit' }
647 655 assert_tag :tag => 'a', :content => 'Closed',
648 656 :attributes => { :href => '/issues/edit/1?issue%5Bstatus_id%5D=5',
649 657 :class => '' }
650 658 assert_tag :tag => 'a', :content => 'Immediate',
651 659 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;priority_id=8',
652 660 :class => '' }
653 661 assert_tag :tag => 'a', :content => 'Dave Lopper',
654 662 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1',
655 663 :class => '' }
656 664 assert_tag :tag => 'a', :content => 'Copy',
657 665 :attributes => { :href => '/projects/ecookbook/issues/new?copy_from=1',
658 666 :class => 'icon-copy' }
659 667 assert_tag :tag => 'a', :content => 'Move',
660 668 :attributes => { :href => '/issues/move?ids%5B%5D=1',
661 669 :class => 'icon-move' }
662 670 assert_tag :tag => 'a', :content => 'Delete',
663 671 :attributes => { :href => '/issues/destroy?ids%5B%5D=1',
664 672 :class => 'icon-del' }
665 673 end
666 674
667 675 def test_context_menu_one_issue_by_anonymous
668 676 get :context_menu, :ids => [1]
669 677 assert_response :success
670 678 assert_template 'context_menu'
671 679 assert_tag :tag => 'a', :content => 'Delete',
672 680 :attributes => { :href => '#',
673 681 :class => 'icon-del disabled' }
674 682 end
675 683
676 684 def test_context_menu_multiple_issues_of_same_project
677 685 @request.session[:user_id] = 2
678 686 get :context_menu, :ids => [1, 2]
679 687 assert_response :success
680 688 assert_template 'context_menu'
681 689 assert_tag :tag => 'a', :content => 'Edit',
682 690 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2',
683 691 :class => 'icon-edit' }
684 692 assert_tag :tag => 'a', :content => 'Immediate',
685 693 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2&amp;priority_id=8',
686 694 :class => '' }
687 695 assert_tag :tag => 'a', :content => 'Dave Lopper',
688 696 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1&amp;ids%5B%5D=2',
689 697 :class => '' }
690 698 assert_tag :tag => 'a', :content => 'Move',
691 699 :attributes => { :href => '/issues/move?ids%5B%5D=1&amp;ids%5B%5D=2',
692 700 :class => 'icon-move' }
693 701 assert_tag :tag => 'a', :content => 'Delete',
694 702 :attributes => { :href => '/issues/destroy?ids%5B%5D=1&amp;ids%5B%5D=2',
695 703 :class => 'icon-del' }
696 704 end
697 705
698 706 def test_context_menu_multiple_issues_of_different_project
699 707 @request.session[:user_id] = 2
700 708 get :context_menu, :ids => [1, 2, 4]
701 709 assert_response :success
702 710 assert_template 'context_menu'
703 711 assert_tag :tag => 'a', :content => 'Delete',
704 712 :attributes => { :href => '#',
705 713 :class => 'icon-del disabled' }
706 714 end
707 715
708 716 def test_destroy_issue_with_no_time_entries
709 717 assert_nil TimeEntry.find_by_issue_id(2)
710 718 @request.session[:user_id] = 2
711 719 post :destroy, :id => 2
712 720 assert_redirected_to 'projects/ecookbook/issues'
713 721 assert_nil Issue.find_by_id(2)
714 722 end
715 723
716 724 def test_destroy_issues_with_time_entries
717 725 @request.session[:user_id] = 2
718 726 post :destroy, :ids => [1, 3]
719 727 assert_response :success
720 728 assert_template 'destroy'
721 729 assert_not_nil assigns(:hours)
722 730 assert Issue.find_by_id(1) && Issue.find_by_id(3)
723 731 end
724 732
725 733 def test_destroy_issues_and_destroy_time_entries
726 734 @request.session[:user_id] = 2
727 735 post :destroy, :ids => [1, 3], :todo => 'destroy'
728 736 assert_redirected_to 'projects/ecookbook/issues'
729 737 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
730 738 assert_nil TimeEntry.find_by_id([1, 2])
731 739 end
732 740
733 741 def test_destroy_issues_and_assign_time_entries_to_project
734 742 @request.session[:user_id] = 2
735 743 post :destroy, :ids => [1, 3], :todo => 'nullify'
736 744 assert_redirected_to 'projects/ecookbook/issues'
737 745 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
738 746 assert_nil TimeEntry.find(1).issue_id
739 747 assert_nil TimeEntry.find(2).issue_id
740 748 end
741 749
742 750 def test_destroy_issues_and_reassign_time_entries_to_another_issue
743 751 @request.session[:user_id] = 2
744 752 post :destroy, :ids => [1, 3], :todo => 'reassign', :reassign_to_id => 2
745 753 assert_redirected_to 'projects/ecookbook/issues'
746 754 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
747 755 assert_equal 2, TimeEntry.find(1).issue_id
748 756 assert_equal 2, TimeEntry.find(2).issue_id
749 757 end
750 758 end
1 NO CONTENT: file was removed
1 NO CONTENT: file was removed
1 NO CONTENT: file was removed
1 NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now