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