##// END OF EJS Templates
PDF: switch TCPDF UTF-8 or FPDF ANSI (#61)....
Toshi MARUYAMA -
r5139:325af8d48cca
parent child
Show More
@@ -1,381 +1,398
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2009 Jean-Philippe Lang
4 # Copyright (C) 2006-2009 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 require 'iconv'
20 require 'iconv'
21 require 'rfpdf/fpdf'
21 require 'rfpdf/fpdf'
22 require 'fpdf/chinese'
22 require 'fpdf/chinese'
23 require 'fpdf/japanese'
23 require 'fpdf/japanese'
24 require 'fpdf/korean'
24 require 'fpdf/korean'
25
25
26 module Redmine
26 module Redmine
27 module Export
27 module Export
28 module PDF
28 module PDF
29 include ActionView::Helpers::TextHelper
29 include ActionView::Helpers::TextHelper
30 include ActionView::Helpers::NumberHelper
30 include ActionView::Helpers::NumberHelper
31
31
32 class ITCPDF < TCPDF
32 class ITCPDF < TCPDF
33 include Redmine::I18n
33 include Redmine::I18n
34 attr_accessor :footer_date
34 attr_accessor :footer_date
35
35
36 def initialize(lang)
36 def initialize(lang)
37 super()
37 super()
38 set_language_if_valid lang
38 set_language_if_valid lang
39 @font_for_content = 'FreeSans'
39 @font_for_content = 'FreeSans'
40 @font_for_footer = 'FreeSans'
40 @font_for_footer = 'FreeSans'
41 SetCreator(Redmine::Info.app_name)
41 SetCreator(Redmine::Info.app_name)
42 SetFont(@font_for_content)
42 SetFont(@font_for_content)
43 end
43 end
44
44
45 def SetFontStyle(style, size)
45 def SetFontStyle(style, size)
46 SetFont(@font_for_content, style, size)
46 SetFont(@font_for_content, style, size)
47 end
47 end
48
48
49 def SetTitle(txt)
49 def SetTitle(txt)
50 txt = begin
50 txt = begin
51 utf16txt = Iconv.conv('UTF-16BE', 'UTF-8', txt)
51 utf16txt = Iconv.conv('UTF-16BE', 'UTF-8', txt)
52 hextxt = "<FEFF" # FEFF is BOM
52 hextxt = "<FEFF" # FEFF is BOM
53 hextxt << utf16txt.unpack("C*").map {|x| sprintf("%02X",x) }.join
53 hextxt << utf16txt.unpack("C*").map {|x| sprintf("%02X",x) }.join
54 hextxt << ">"
54 hextxt << ">"
55 rescue
55 rescue
56 txt
56 txt
57 end || ''
57 end || ''
58 super(txt)
58 super(txt)
59 end
59 end
60
60
61 def textstring(s)
61 def textstring(s)
62 # Format a text string
62 # Format a text string
63 if s =~ /^</ # This means the string is hex-dumped.
63 if s =~ /^</ # This means the string is hex-dumped.
64 return s
64 return s
65 else
65 else
66 return '('+escape(s)+')'
66 return '('+escape(s)+')'
67 end
67 end
68 end
68 end
69
69
70 alias RDMCell Cell
70 alias RDMCell Cell
71 alias RDMMultiCell MultiCell
71 alias RDMMultiCell MultiCell
72
72
73 def Footer
73 def Footer
74 SetFont(@font_for_footer, 'I', 8)
74 SetFont(@font_for_footer, 'I', 8)
75 SetY(-15)
75 SetY(-15)
76 SetX(15)
76 SetX(15)
77 RDMCell(0, 5, @footer_date, 0, 0, 'L')
77 RDMCell(0, 5, @footer_date, 0, 0, 'L')
78 SetY(-15)
78 SetY(-15)
79 SetX(-30)
79 SetX(-30)
80 RDMCell(0, 5, PageNo().to_s + '/{nb}', 0, 0, 'C')
80 RDMCell(0, 5, PageNo().to_s + '/{nb}', 0, 0, 'C')
81 end
81 end
82 end
82 end
83
83
84 class IFPDF < FPDF
84 class IFPDF < FPDF
85 include Redmine::I18n
85 include Redmine::I18n
86 attr_accessor :footer_date
86 attr_accessor :footer_date
87
87
88 def initialize(lang)
88 def initialize(lang)
89 super()
89 super()
90 set_language_if_valid lang
90 set_language_if_valid lang
91 case current_language.to_s.downcase
91 case current_language.to_s.downcase
92 when 'ko'
92 when 'ko'
93 extend(PDF_Korean)
93 extend(PDF_Korean)
94 AddUHCFont()
94 AddUHCFont()
95 @font_for_content = 'UHC'
95 @font_for_content = 'UHC'
96 @font_for_footer = 'UHC'
96 @font_for_footer = 'UHC'
97 when 'ja'
97 when 'ja'
98 extend(PDF_Japanese)
98 extend(PDF_Japanese)
99 AddSJISFont()
99 AddSJISFont()
100 @font_for_content = 'SJIS'
100 @font_for_content = 'SJIS'
101 @font_for_footer = 'SJIS'
101 @font_for_footer = 'SJIS'
102 when 'zh'
102 when 'zh'
103 extend(PDF_Chinese)
103 extend(PDF_Chinese)
104 AddGBFont()
104 AddGBFont()
105 @font_for_content = 'GB'
105 @font_for_content = 'GB'
106 @font_for_footer = 'GB'
106 @font_for_footer = 'GB'
107 when 'zh-tw'
107 when 'zh-tw'
108 extend(PDF_Chinese)
108 extend(PDF_Chinese)
109 AddBig5Font()
109 AddBig5Font()
110 @font_for_content = 'Big5'
110 @font_for_content = 'Big5'
111 @font_for_footer = 'Big5'
111 @font_for_footer = 'Big5'
112 else
112 else
113 @font_for_content = 'Arial'
113 @font_for_content = 'Arial'
114 @font_for_footer = 'Helvetica'
114 @font_for_footer = 'Helvetica'
115 end
115 end
116 SetCreator(Redmine::Info.app_name)
116 SetCreator(Redmine::Info.app_name)
117 SetFont(@font_for_content)
117 SetFont(@font_for_content)
118 end
118 end
119
119
120 def SetFontStyle(style, size)
120 def SetFontStyle(style, size)
121 SetFont(@font_for_content, style, size)
121 SetFont(@font_for_content, style, size)
122 end
122 end
123
123
124 def SetTitle(txt)
124 def SetTitle(txt)
125 txt = begin
125 txt = begin
126 utf16txt = Iconv.conv('UTF-16BE', 'UTF-8', txt)
126 utf16txt = Iconv.conv('UTF-16BE', 'UTF-8', txt)
127 hextxt = "<FEFF" # FEFF is BOM
127 hextxt = "<FEFF" # FEFF is BOM
128 hextxt << utf16txt.unpack("C*").map {|x| sprintf("%02X",x) }.join
128 hextxt << utf16txt.unpack("C*").map {|x| sprintf("%02X",x) }.join
129 hextxt << ">"
129 hextxt << ">"
130 rescue
130 rescue
131 txt
131 txt
132 end || ''
132 end || ''
133 super(txt)
133 super(txt)
134 end
134 end
135
135
136 def textstring(s)
136 def textstring(s)
137 # Format a text string
137 # Format a text string
138 if s =~ /^</ # This means the string is hex-dumped.
138 if s =~ /^</ # This means the string is hex-dumped.
139 return s
139 return s
140 else
140 else
141 return '('+escape(s)+')'
141 return '('+escape(s)+')'
142 end
142 end
143 end
143 end
144
144
145 def fix_text_encoding(txt)
145 def fix_text_encoding(txt)
146 @ic ||= Iconv.new(l(:general_pdf_encoding), 'UTF-8')
146 @ic ||= Iconv.new(l(:general_pdf_encoding), 'UTF-8')
147 # these quotation marks are not correctly rendered in the pdf
147 # these quotation marks are not correctly rendered in the pdf
148 txt = txt.gsub(/[Ò€œÒ€�]/, '"') if txt
148 txt = txt.gsub(/[Ò€œÒ€�]/, '"') if txt
149 txt = begin
149 txt = begin
150 # 0x5c char handling
150 # 0x5c char handling
151 txtar = txt.split('\\')
151 txtar = txt.split('\\')
152 txtar << '' if txt[-1] == ?\\
152 txtar << '' if txt[-1] == ?\\
153 txtar.collect {|x| @ic.iconv(x)}.join('\\').gsub(/\\/, "\\\\\\\\")
153 txtar.collect {|x| @ic.iconv(x)}.join('\\').gsub(/\\/, "\\\\\\\\")
154 rescue
154 rescue
155 txt
155 txt
156 end || ''
156 end || ''
157 return txt
157 return txt
158 end
158 end
159
159
160 def RDMCell(w,h=0,txt='',border=0,ln=0,align='',fill=0,link='')
160 def RDMCell(w,h=0,txt='',border=0,ln=0,align='',fill=0,link='')
161 Cell(w,h,fix_text_encoding(txt),border,ln,align,fill,link)
161 Cell(w,h,fix_text_encoding(txt),border,ln,align,fill,link)
162 end
162 end
163
163
164 def RDMMultiCell(w,h=0,txt='',border=0,align='',fill=0)
164 def RDMMultiCell(w,h=0,txt='',border=0,align='',fill=0)
165 MultiCell(w,h,fix_text_encoding(txt),border,align,fill)
165 MultiCell(w,h,fix_text_encoding(txt),border,align,fill)
166 end
166 end
167
167
168 def Footer
168 def Footer
169 SetFont(@font_for_footer, 'I', 8)
169 SetFont(@font_for_footer, 'I', 8)
170 SetY(-15)
170 SetY(-15)
171 SetX(15)
171 SetX(15)
172 RDMCell(0, 5, @footer_date, 0, 0, 'L')
172 RDMCell(0, 5, @footer_date, 0, 0, 'L')
173 SetY(-15)
173 SetY(-15)
174 SetX(-30)
174 SetX(-30)
175 RDMCell(0, 5, PageNo().to_s + '/{nb}', 0, 0, 'C')
175 RDMCell(0, 5, PageNo().to_s + '/{nb}', 0, 0, 'C')
176 end
176 end
177 alias alias_nb_pages AliasNbPages
177 alias alias_nb_pages AliasNbPages
178 end
178 end
179
179
180 # Returns a PDF string of a list of issues
180 # Returns a PDF string of a list of issues
181 def issues_to_pdf(issues, project, query)
181 def issues_to_pdf(issues, project, query)
182 if Redmine::Platform.mswin? ||
183 ( current_language.to_s.downcase == 'ko' ||
184 current_language.to_s.downcase == 'ja' ||
185 current_language.to_s.downcase == 'zh' ||
186 current_language.to_s.downcase == 'zh-tw' ||
187 current_language.to_s.downcase == 'th' )
182 pdf = IFPDF.new(current_language)
188 pdf = IFPDF.new(current_language)
183
189 else
190 pdf = ITCPDF.new(current_language)
191 end
184 title = query.new_record? ? l(:label_issue_plural) : query.name
192 title = query.new_record? ? l(:label_issue_plural) : query.name
185 title = "#{project} - #{title}" if project
193 title = "#{project} - #{title}" if project
186 pdf.SetTitle(title)
194 pdf.SetTitle(title)
187 pdf.alias_nb_pages
195 pdf.alias_nb_pages
188 pdf.footer_date = format_date(Date.today)
196 pdf.footer_date = format_date(Date.today)
189 pdf.AddPage("L")
197 pdf.AddPage("L")
190
198
191 row_height = 6
199 row_height = 6
192 col_width = []
200 col_width = []
193 unless query.columns.empty?
201 unless query.columns.empty?
194 col_width = query.columns.collect {|column| column.name == :subject ? 4.0 : 1.0 }
202 col_width = query.columns.collect {|column| column.name == :subject ? 4.0 : 1.0 }
195 ratio = 262.0 / col_width.inject(0) {|s,w| s += w}
203 ratio = 262.0 / col_width.inject(0) {|s,w| s += w}
196 col_width = col_width.collect {|w| w * ratio}
204 col_width = col_width.collect {|w| w * ratio}
197 end
205 end
198
206
199 # title
207 # title
200 pdf.SetFontStyle('B',11)
208 pdf.SetFontStyle('B',11)
201 pdf.RDMCell(190,10, title)
209 pdf.RDMCell(190,10, title)
202 pdf.Ln
210 pdf.Ln
203
211
204 # headers
212 # headers
205 pdf.SetFontStyle('B',8)
213 pdf.SetFontStyle('B',8)
206 pdf.SetFillColor(230, 230, 230)
214 pdf.SetFillColor(230, 230, 230)
207 pdf.RDMCell(15, row_height, "#", 1, 0, 'L', 1)
215 pdf.RDMCell(15, row_height, "#", 1, 0, 'L', 1)
208 query.columns.each_with_index do |column, i|
216 query.columns.each_with_index do |column, i|
209 pdf.RDMCell(col_width[i], row_height, column.caption, 1, 0, 'L', 1)
217 pdf.RDMCell(col_width[i], row_height, column.caption, 1, 0, 'L', 1)
210 end
218 end
211 pdf.Ln
219 pdf.Ln
212
220
213 # rows
221 # rows
214 pdf.SetFontStyle('',8)
222 pdf.SetFontStyle('',8)
215 pdf.SetFillColor(255, 255, 255)
223 pdf.SetFillColor(255, 255, 255)
216 previous_group = false
224 previous_group = false
217 issues.each do |issue|
225 issues.each do |issue|
218 if query.grouped? && (group = query.group_by_column.value(issue)) != previous_group
226 if query.grouped? && (group = query.group_by_column.value(issue)) != previous_group
219 pdf.SetFontStyle('B',9)
227 pdf.SetFontStyle('B',9)
220 pdf.RDMCell(277, row_height,
228 pdf.RDMCell(277, row_height,
221 (group.blank? ? 'None' : group.to_s) + " (#{query.issue_count_by_group[group]})",
229 (group.blank? ? 'None' : group.to_s) + " (#{query.issue_count_by_group[group]})",
222 1, 1, 'L')
230 1, 1, 'L')
223 pdf.SetFontStyle('',8)
231 pdf.SetFontStyle('',8)
224 previous_group = group
232 previous_group = group
225 end
233 end
226 pdf.RDMCell(15, row_height, issue.id.to_s, 1, 0, 'L', 1)
234 pdf.RDMCell(15, row_height, issue.id.to_s, 1, 0, 'L', 1)
227 query.columns.each_with_index do |column, i|
235 query.columns.each_with_index do |column, i|
228 s = if column.is_a?(QueryCustomFieldColumn)
236 s = if column.is_a?(QueryCustomFieldColumn)
229 cv = issue.custom_values.detect {|v| v.custom_field_id == column.custom_field.id}
237 cv = issue.custom_values.detect {|v| v.custom_field_id == column.custom_field.id}
230 show_value(cv)
238 show_value(cv)
231 else
239 else
232 value = issue.send(column.name)
240 value = issue.send(column.name)
233 if value.is_a?(Date)
241 if value.is_a?(Date)
234 format_date(value)
242 format_date(value)
235 elsif value.is_a?(Time)
243 elsif value.is_a?(Time)
236 format_time(value)
244 format_time(value)
237 else
245 else
238 value
246 value
239 end
247 end
240 end
248 end
241 pdf.RDMCell(col_width[i], row_height, s.to_s, 1, 0, 'L', 1)
249 pdf.RDMCell(col_width[i], row_height, s.to_s, 1, 0, 'L', 1)
242 end
250 end
243 pdf.Ln
251 pdf.Ln
244 end
252 end
245 if issues.size == Setting.issues_export_limit.to_i
253 if issues.size == Setting.issues_export_limit.to_i
246 pdf.SetFontStyle('B',10)
254 pdf.SetFontStyle('B',10)
247 pdf.RDMCell(0, row_height, '...')
255 pdf.RDMCell(0, row_height, '...')
248 end
256 end
249 pdf.Output
257 pdf.Output
250 end
258 end
251
259
252 # Returns a PDF string of a single issue
260 # Returns a PDF string of a single issue
253 def issue_to_pdf(issue)
261 def issue_to_pdf(issue)
262 if Redmine::Platform.mswin? ||
263 ( current_language.to_s.downcase == 'ko' ||
264 current_language.to_s.downcase == 'ja' ||
265 current_language.to_s.downcase == 'zh' ||
266 current_language.to_s.downcase == 'zh-tw' ||
267 current_language.to_s.downcase == 'th' )
254 pdf = IFPDF.new(current_language)
268 pdf = IFPDF.new(current_language)
269 else
270 pdf = ITCPDF.new(current_language)
271 end
255 pdf.SetTitle("#{issue.project} - ##{issue.tracker} #{issue.id}")
272 pdf.SetTitle("#{issue.project} - ##{issue.tracker} #{issue.id}")
256 pdf.alias_nb_pages
273 pdf.alias_nb_pages
257 pdf.footer_date = format_date(Date.today)
274 pdf.footer_date = format_date(Date.today)
258 pdf.AddPage
275 pdf.AddPage
259
276
260 pdf.SetFontStyle('B',11)
277 pdf.SetFontStyle('B',11)
261 pdf.RDMCell(190,10, "#{issue.project} - #{issue.tracker} # #{issue.id}: #{issue.subject}")
278 pdf.RDMCell(190,10, "#{issue.project} - #{issue.tracker} # #{issue.id}: #{issue.subject}")
262 pdf.Ln
279 pdf.Ln
263
280
264 y0 = pdf.GetY
281 y0 = pdf.GetY
265
282
266 pdf.SetFontStyle('B',9)
283 pdf.SetFontStyle('B',9)
267 pdf.RDMCell(35,5, l(:field_status) + ":","LT")
284 pdf.RDMCell(35,5, l(:field_status) + ":","LT")
268 pdf.SetFontStyle('',9)
285 pdf.SetFontStyle('',9)
269 pdf.RDMCell(60,5, issue.status.to_s,"RT")
286 pdf.RDMCell(60,5, issue.status.to_s,"RT")
270 pdf.SetFontStyle('B',9)
287 pdf.SetFontStyle('B',9)
271 pdf.RDMCell(35,5, l(:field_priority) + ":","LT")
288 pdf.RDMCell(35,5, l(:field_priority) + ":","LT")
272 pdf.SetFontStyle('',9)
289 pdf.SetFontStyle('',9)
273 pdf.RDMCell(60,5, issue.priority.to_s,"RT")
290 pdf.RDMCell(60,5, issue.priority.to_s,"RT")
274 pdf.Ln
291 pdf.Ln
275
292
276 pdf.SetFontStyle('B',9)
293 pdf.SetFontStyle('B',9)
277 pdf.RDMCell(35,5, l(:field_author) + ":","L")
294 pdf.RDMCell(35,5, l(:field_author) + ":","L")
278 pdf.SetFontStyle('',9)
295 pdf.SetFontStyle('',9)
279 pdf.RDMCell(60,5, issue.author.to_s,"R")
296 pdf.RDMCell(60,5, issue.author.to_s,"R")
280 pdf.SetFontStyle('B',9)
297 pdf.SetFontStyle('B',9)
281 pdf.RDMCell(35,5, l(:field_category) + ":","L")
298 pdf.RDMCell(35,5, l(:field_category) + ":","L")
282 pdf.SetFontStyle('',9)
299 pdf.SetFontStyle('',9)
283 pdf.RDMCell(60,5, issue.category.to_s,"R")
300 pdf.RDMCell(60,5, issue.category.to_s,"R")
284 pdf.Ln
301 pdf.Ln
285
302
286 pdf.SetFontStyle('B',9)
303 pdf.SetFontStyle('B',9)
287 pdf.RDMCell(35,5, l(:field_created_on) + ":","L")
304 pdf.RDMCell(35,5, l(:field_created_on) + ":","L")
288 pdf.SetFontStyle('',9)
305 pdf.SetFontStyle('',9)
289 pdf.RDMCell(60,5, format_date(issue.created_on),"R")
306 pdf.RDMCell(60,5, format_date(issue.created_on),"R")
290 pdf.SetFontStyle('B',9)
307 pdf.SetFontStyle('B',9)
291 pdf.RDMCell(35,5, l(:field_assigned_to) + ":","L")
308 pdf.RDMCell(35,5, l(:field_assigned_to) + ":","L")
292 pdf.SetFontStyle('',9)
309 pdf.SetFontStyle('',9)
293 pdf.RDMCell(60,5, issue.assigned_to.to_s,"R")
310 pdf.RDMCell(60,5, issue.assigned_to.to_s,"R")
294 pdf.Ln
311 pdf.Ln
295
312
296 pdf.SetFontStyle('B',9)
313 pdf.SetFontStyle('B',9)
297 pdf.RDMCell(35,5, l(:field_updated_on) + ":","LB")
314 pdf.RDMCell(35,5, l(:field_updated_on) + ":","LB")
298 pdf.SetFontStyle('',9)
315 pdf.SetFontStyle('',9)
299 pdf.RDMCell(60,5, format_date(issue.updated_on),"RB")
316 pdf.RDMCell(60,5, format_date(issue.updated_on),"RB")
300 pdf.SetFontStyle('B',9)
317 pdf.SetFontStyle('B',9)
301 pdf.RDMCell(35,5, l(:field_due_date) + ":","LB")
318 pdf.RDMCell(35,5, l(:field_due_date) + ":","LB")
302 pdf.SetFontStyle('',9)
319 pdf.SetFontStyle('',9)
303 pdf.RDMCell(60,5, format_date(issue.due_date),"RB")
320 pdf.RDMCell(60,5, format_date(issue.due_date),"RB")
304 pdf.Ln
321 pdf.Ln
305
322
306 for custom_value in issue.custom_field_values
323 for custom_value in issue.custom_field_values
307 pdf.SetFontStyle('B',9)
324 pdf.SetFontStyle('B',9)
308 pdf.RDMCell(35,5, custom_value.custom_field.name + ":","L")
325 pdf.RDMCell(35,5, custom_value.custom_field.name + ":","L")
309 pdf.SetFontStyle('',9)
326 pdf.SetFontStyle('',9)
310 pdf.RDMMultiCell(155,5, (show_value custom_value),"R")
327 pdf.RDMMultiCell(155,5, (show_value custom_value),"R")
311 end
328 end
312
329
313 pdf.SetFontStyle('B',9)
330 pdf.SetFontStyle('B',9)
314 pdf.RDMCell(35,5, l(:field_subject) + ":","LTB")
331 pdf.RDMCell(35,5, l(:field_subject) + ":","LTB")
315 pdf.SetFontStyle('',9)
332 pdf.SetFontStyle('',9)
316 pdf.RDMCell(155,5, issue.subject,"RTB")
333 pdf.RDMCell(155,5, issue.subject,"RTB")
317 pdf.Ln
334 pdf.Ln
318
335
319 pdf.SetFontStyle('B',9)
336 pdf.SetFontStyle('B',9)
320 pdf.RDMCell(35,5, l(:field_description) + ":")
337 pdf.RDMCell(35,5, l(:field_description) + ":")
321 pdf.SetFontStyle('',9)
338 pdf.SetFontStyle('',9)
322 pdf.RDMMultiCell(155,5, issue.description.to_s,"BR")
339 pdf.RDMMultiCell(155,5, issue.description.to_s,"BR")
323
340
324 pdf.Line(pdf.GetX, y0, pdf.GetX, pdf.GetY)
341 pdf.Line(pdf.GetX, y0, pdf.GetX, pdf.GetY)
325 pdf.Line(pdf.GetX, pdf.GetY, 170, pdf.GetY)
342 pdf.Line(pdf.GetX, pdf.GetY, 170, pdf.GetY)
326 pdf.Ln
343 pdf.Ln
327
344
328 if issue.changesets.any? && User.current.allowed_to?(:view_changesets, issue.project)
345 if issue.changesets.any? && User.current.allowed_to?(:view_changesets, issue.project)
329 pdf.SetFontStyle('B',9)
346 pdf.SetFontStyle('B',9)
330 pdf.RDMCell(190,5, l(:label_associated_revisions), "B")
347 pdf.RDMCell(190,5, l(:label_associated_revisions), "B")
331 pdf.Ln
348 pdf.Ln
332 for changeset in issue.changesets
349 for changeset in issue.changesets
333 pdf.SetFontStyle('B',8)
350 pdf.SetFontStyle('B',8)
334 pdf.RDMCell(190,5, format_time(changeset.committed_on) + " - " + changeset.author.to_s)
351 pdf.RDMCell(190,5, format_time(changeset.committed_on) + " - " + changeset.author.to_s)
335 pdf.Ln
352 pdf.Ln
336 unless changeset.comments.blank?
353 unless changeset.comments.blank?
337 pdf.SetFontStyle('',8)
354 pdf.SetFontStyle('',8)
338 pdf.RDMMultiCell(190,5, changeset.comments.to_s)
355 pdf.RDMMultiCell(190,5, changeset.comments.to_s)
339 end
356 end
340 pdf.Ln
357 pdf.Ln
341 end
358 end
342 end
359 end
343
360
344 pdf.SetFontStyle('B',9)
361 pdf.SetFontStyle('B',9)
345 pdf.RDMCell(190,5, l(:label_history), "B")
362 pdf.RDMCell(190,5, l(:label_history), "B")
346 pdf.Ln
363 pdf.Ln
347 for journal in issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
364 for journal in issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
348 pdf.SetFontStyle('B',8)
365 pdf.SetFontStyle('B',8)
349 pdf.RDMCell(190,5, format_time(journal.created_on) + " - " + journal.user.name)
366 pdf.RDMCell(190,5, format_time(journal.created_on) + " - " + journal.user.name)
350 pdf.Ln
367 pdf.Ln
351 pdf.SetFontStyle('I',8)
368 pdf.SetFontStyle('I',8)
352 for detail in journal.details
369 for detail in journal.details
353 pdf.RDMCell(190,5, "- " + show_detail(detail, true))
370 pdf.RDMCell(190,5, "- " + show_detail(detail, true))
354 pdf.Ln
371 pdf.Ln
355 end
372 end
356 if journal.notes?
373 if journal.notes?
357 pdf.SetFontStyle('',8)
374 pdf.SetFontStyle('',8)
358 pdf.RDMMultiCell(190,5, journal.notes.to_s)
375 pdf.RDMMultiCell(190,5, journal.notes.to_s)
359 end
376 end
360 pdf.Ln
377 pdf.Ln
361 end
378 end
362
379
363 if issue.attachments.any?
380 if issue.attachments.any?
364 pdf.SetFontStyle('B',9)
381 pdf.SetFontStyle('B',9)
365 pdf.RDMCell(190,5, l(:label_attachment_plural), "B")
382 pdf.RDMCell(190,5, l(:label_attachment_plural), "B")
366 pdf.Ln
383 pdf.Ln
367 for attachment in issue.attachments
384 for attachment in issue.attachments
368 pdf.SetFontStyle('',8)
385 pdf.SetFontStyle('',8)
369 pdf.RDMCell(80,5, attachment.filename)
386 pdf.RDMCell(80,5, attachment.filename)
370 pdf.RDMCell(20,5, number_to_human_size(attachment.filesize),0,0,"R")
387 pdf.RDMCell(20,5, number_to_human_size(attachment.filesize),0,0,"R")
371 pdf.RDMCell(25,5, format_date(attachment.created_on),0,0,"R")
388 pdf.RDMCell(25,5, format_date(attachment.created_on),0,0,"R")
372 pdf.RDMCell(65,5, attachment.author.name,0,0,"R")
389 pdf.RDMCell(65,5, attachment.author.name,0,0,"R")
373 pdf.Ln
390 pdf.Ln
374 end
391 end
375 end
392 end
376 pdf.Output
393 pdf.Output
377 end
394 end
378
395
379 end
396 end
380 end
397 end
381 end
398 end
@@ -1,860 +1,869
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 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 module Redmine
18 module Redmine
19 module Helpers
19 module Helpers
20 # Simple class to handle gantt chart data
20 # Simple class to handle gantt chart data
21 class Gantt
21 class Gantt
22 include ERB::Util
22 include ERB::Util
23 include Redmine::I18n
23 include Redmine::I18n
24
24
25 # :nodoc:
25 # :nodoc:
26 # Some utility methods for the PDF export
26 # Some utility methods for the PDF export
27 class PDF
27 class PDF
28 MaxCharactorsForSubject = 45
28 MaxCharactorsForSubject = 45
29 TotalWidth = 280
29 TotalWidth = 280
30 LeftPaneWidth = 100
30 LeftPaneWidth = 100
31
31
32 def self.right_pane_width
32 def self.right_pane_width
33 TotalWidth - LeftPaneWidth
33 TotalWidth - LeftPaneWidth
34 end
34 end
35 end
35 end
36
36
37 attr_reader :year_from, :month_from, :date_from, :date_to, :zoom, :months, :truncated, :max_rows
37 attr_reader :year_from, :month_from, :date_from, :date_to, :zoom, :months, :truncated, :max_rows
38 attr_accessor :query
38 attr_accessor :query
39 attr_accessor :project
39 attr_accessor :project
40 attr_accessor :view
40 attr_accessor :view
41
41
42 def initialize(options={})
42 def initialize(options={})
43 options = options.dup
43 options = options.dup
44
44
45 if options[:year] && options[:year].to_i >0
45 if options[:year] && options[:year].to_i >0
46 @year_from = options[:year].to_i
46 @year_from = options[:year].to_i
47 if options[:month] && options[:month].to_i >=1 && options[:month].to_i <= 12
47 if options[:month] && options[:month].to_i >=1 && options[:month].to_i <= 12
48 @month_from = options[:month].to_i
48 @month_from = options[:month].to_i
49 else
49 else
50 @month_from = 1
50 @month_from = 1
51 end
51 end
52 else
52 else
53 @month_from ||= Date.today.month
53 @month_from ||= Date.today.month
54 @year_from ||= Date.today.year
54 @year_from ||= Date.today.year
55 end
55 end
56
56
57 zoom = (options[:zoom] || User.current.pref[:gantt_zoom]).to_i
57 zoom = (options[:zoom] || User.current.pref[:gantt_zoom]).to_i
58 @zoom = (zoom > 0 && zoom < 5) ? zoom : 2
58 @zoom = (zoom > 0 && zoom < 5) ? zoom : 2
59 months = (options[:months] || User.current.pref[:gantt_months]).to_i
59 months = (options[:months] || User.current.pref[:gantt_months]).to_i
60 @months = (months > 0 && months < 25) ? months : 6
60 @months = (months > 0 && months < 25) ? months : 6
61
61
62 # Save gantt parameters as user preference (zoom and months count)
62 # Save gantt parameters as user preference (zoom and months count)
63 if (User.current.logged? && (@zoom != User.current.pref[:gantt_zoom] || @months != User.current.pref[:gantt_months]))
63 if (User.current.logged? && (@zoom != User.current.pref[:gantt_zoom] || @months != User.current.pref[:gantt_months]))
64 User.current.pref[:gantt_zoom], User.current.pref[:gantt_months] = @zoom, @months
64 User.current.pref[:gantt_zoom], User.current.pref[:gantt_months] = @zoom, @months
65 User.current.preference.save
65 User.current.preference.save
66 end
66 end
67
67
68 @date_from = Date.civil(@year_from, @month_from, 1)
68 @date_from = Date.civil(@year_from, @month_from, 1)
69 @date_to = (@date_from >> @months) - 1
69 @date_to = (@date_from >> @months) - 1
70
70
71 @subjects = ''
71 @subjects = ''
72 @lines = ''
72 @lines = ''
73 @number_of_rows = nil
73 @number_of_rows = nil
74
74
75 @issue_ancestors = []
75 @issue_ancestors = []
76
76
77 @truncated = false
77 @truncated = false
78 if options.has_key?(:max_rows)
78 if options.has_key?(:max_rows)
79 @max_rows = options[:max_rows]
79 @max_rows = options[:max_rows]
80 else
80 else
81 @max_rows = Setting.gantt_items_limit.blank? ? nil : Setting.gantt_items_limit.to_i
81 @max_rows = Setting.gantt_items_limit.blank? ? nil : Setting.gantt_items_limit.to_i
82 end
82 end
83 end
83 end
84
84
85 def common_params
85 def common_params
86 { :controller => 'gantts', :action => 'show', :project_id => @project }
86 { :controller => 'gantts', :action => 'show', :project_id => @project }
87 end
87 end
88
88
89 def params
89 def params
90 common_params.merge({ :zoom => zoom, :year => year_from, :month => month_from, :months => months })
90 common_params.merge({ :zoom => zoom, :year => year_from, :month => month_from, :months => months })
91 end
91 end
92
92
93 def params_previous
93 def params_previous
94 common_params.merge({:year => (date_from << months).year, :month => (date_from << months).month, :zoom => zoom, :months => months })
94 common_params.merge({:year => (date_from << months).year, :month => (date_from << months).month, :zoom => zoom, :months => months })
95 end
95 end
96
96
97 def params_next
97 def params_next
98 common_params.merge({:year => (date_from >> months).year, :month => (date_from >> months).month, :zoom => zoom, :months => months })
98 common_params.merge({:year => (date_from >> months).year, :month => (date_from >> months).month, :zoom => zoom, :months => months })
99 end
99 end
100
100
101 # Returns the number of rows that will be rendered on the Gantt chart
101 # Returns the number of rows that will be rendered on the Gantt chart
102 def number_of_rows
102 def number_of_rows
103 return @number_of_rows if @number_of_rows
103 return @number_of_rows if @number_of_rows
104
104
105 rows = projects.inject(0) {|total, p| total += number_of_rows_on_project(p)}
105 rows = projects.inject(0) {|total, p| total += number_of_rows_on_project(p)}
106 rows > @max_rows ? @max_rows : rows
106 rows > @max_rows ? @max_rows : rows
107 end
107 end
108
108
109 # Returns the number of rows that will be used to list a project on
109 # Returns the number of rows that will be used to list a project on
110 # the Gantt chart. This will recurse for each subproject.
110 # the Gantt chart. This will recurse for each subproject.
111 def number_of_rows_on_project(project)
111 def number_of_rows_on_project(project)
112 return 0 unless projects.include?(project)
112 return 0 unless projects.include?(project)
113
113
114 count = 1
114 count = 1
115 count += project_issues(project).size
115 count += project_issues(project).size
116 count += project_versions(project).size
116 count += project_versions(project).size
117 count
117 count
118 end
118 end
119
119
120 # Renders the subjects of the Gantt chart, the left side.
120 # Renders the subjects of the Gantt chart, the left side.
121 def subjects(options={})
121 def subjects(options={})
122 render(options.merge(:only => :subjects)) unless @subjects_rendered
122 render(options.merge(:only => :subjects)) unless @subjects_rendered
123 @subjects
123 @subjects
124 end
124 end
125
125
126 # Renders the lines of the Gantt chart, the right side
126 # Renders the lines of the Gantt chart, the right side
127 def lines(options={})
127 def lines(options={})
128 render(options.merge(:only => :lines)) unless @lines_rendered
128 render(options.merge(:only => :lines)) unless @lines_rendered
129 @lines
129 @lines
130 end
130 end
131
131
132 # Returns issues that will be rendered
132 # Returns issues that will be rendered
133 def issues
133 def issues
134 @issues ||= @query.issues(
134 @issues ||= @query.issues(
135 :include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
135 :include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
136 :order => "#{Project.table_name}.lft ASC, #{Issue.table_name}.id ASC",
136 :order => "#{Project.table_name}.lft ASC, #{Issue.table_name}.id ASC",
137 :limit => @max_rows
137 :limit => @max_rows
138 )
138 )
139 end
139 end
140
140
141 # Return all the project nodes that will be displayed
141 # Return all the project nodes that will be displayed
142 def projects
142 def projects
143 return @projects if @projects
143 return @projects if @projects
144
144
145 ids = issues.collect(&:project).uniq.collect(&:id)
145 ids = issues.collect(&:project).uniq.collect(&:id)
146 if ids.any?
146 if ids.any?
147 # All issues projects and their visible ancestors
147 # All issues projects and their visible ancestors
148 @projects = Project.visible.all(
148 @projects = Project.visible.all(
149 :joins => "LEFT JOIN #{Project.table_name} child ON #{Project.table_name}.lft <= child.lft AND #{Project.table_name}.rgt >= child.rgt",
149 :joins => "LEFT JOIN #{Project.table_name} child ON #{Project.table_name}.lft <= child.lft AND #{Project.table_name}.rgt >= child.rgt",
150 :conditions => ["child.id IN (?)", ids],
150 :conditions => ["child.id IN (?)", ids],
151 :order => "#{Project.table_name}.lft ASC"
151 :order => "#{Project.table_name}.lft ASC"
152 ).uniq
152 ).uniq
153 else
153 else
154 @projects = []
154 @projects = []
155 end
155 end
156 end
156 end
157
157
158 # Returns the issues that belong to +project+
158 # Returns the issues that belong to +project+
159 def project_issues(project)
159 def project_issues(project)
160 @issues_by_project ||= issues.group_by(&:project)
160 @issues_by_project ||= issues.group_by(&:project)
161 @issues_by_project[project] || []
161 @issues_by_project[project] || []
162 end
162 end
163
163
164 # Returns the distinct versions of the issues that belong to +project+
164 # Returns the distinct versions of the issues that belong to +project+
165 def project_versions(project)
165 def project_versions(project)
166 project_issues(project).collect(&:fixed_version).compact.uniq
166 project_issues(project).collect(&:fixed_version).compact.uniq
167 end
167 end
168
168
169 # Returns the issues that belong to +project+ and are assigned to +version+
169 # Returns the issues that belong to +project+ and are assigned to +version+
170 def version_issues(project, version)
170 def version_issues(project, version)
171 project_issues(project).select {|issue| issue.fixed_version == version}
171 project_issues(project).select {|issue| issue.fixed_version == version}
172 end
172 end
173
173
174 def render(options={})
174 def render(options={})
175 options = {:top => 0, :top_increment => 20, :indent_increment => 20, :render => :subject, :format => :html}.merge(options)
175 options = {:top => 0, :top_increment => 20, :indent_increment => 20, :render => :subject, :format => :html}.merge(options)
176 indent = options[:indent] || 4
176 indent = options[:indent] || 4
177
177
178 @subjects = '' unless options[:only] == :lines
178 @subjects = '' unless options[:only] == :lines
179 @lines = '' unless options[:only] == :subjects
179 @lines = '' unless options[:only] == :subjects
180 @number_of_rows = 0
180 @number_of_rows = 0
181
181
182 Project.project_tree(projects) do |project, level|
182 Project.project_tree(projects) do |project, level|
183 options[:indent] = indent + level * options[:indent_increment]
183 options[:indent] = indent + level * options[:indent_increment]
184 render_project(project, options)
184 render_project(project, options)
185 break if abort?
185 break if abort?
186 end
186 end
187
187
188 @subjects_rendered = true unless options[:only] == :lines
188 @subjects_rendered = true unless options[:only] == :lines
189 @lines_rendered = true unless options[:only] == :subjects
189 @lines_rendered = true unless options[:only] == :subjects
190
190
191 render_end(options)
191 render_end(options)
192 end
192 end
193
193
194 def render_project(project, options={})
194 def render_project(project, options={})
195 subject_for_project(project, options) unless options[:only] == :lines
195 subject_for_project(project, options) unless options[:only] == :lines
196 line_for_project(project, options) unless options[:only] == :subjects
196 line_for_project(project, options) unless options[:only] == :subjects
197
197
198 options[:top] += options[:top_increment]
198 options[:top] += options[:top_increment]
199 options[:indent] += options[:indent_increment]
199 options[:indent] += options[:indent_increment]
200 @number_of_rows += 1
200 @number_of_rows += 1
201 return if abort?
201 return if abort?
202
202
203 issues = project_issues(project).select {|i| i.fixed_version.nil?}
203 issues = project_issues(project).select {|i| i.fixed_version.nil?}
204 sort_issues!(issues)
204 sort_issues!(issues)
205 if issues
205 if issues
206 render_issues(issues, options)
206 render_issues(issues, options)
207 return if abort?
207 return if abort?
208 end
208 end
209
209
210 versions = project_versions(project)
210 versions = project_versions(project)
211 versions.each do |version|
211 versions.each do |version|
212 render_version(project, version, options)
212 render_version(project, version, options)
213 end
213 end
214
214
215 # Remove indent to hit the next sibling
215 # Remove indent to hit the next sibling
216 options[:indent] -= options[:indent_increment]
216 options[:indent] -= options[:indent_increment]
217 end
217 end
218
218
219 def render_issues(issues, options={})
219 def render_issues(issues, options={})
220 @issue_ancestors = []
220 @issue_ancestors = []
221
221
222 issues.each do |i|
222 issues.each do |i|
223 subject_for_issue(i, options) unless options[:only] == :lines
223 subject_for_issue(i, options) unless options[:only] == :lines
224 line_for_issue(i, options) unless options[:only] == :subjects
224 line_for_issue(i, options) unless options[:only] == :subjects
225
225
226 options[:top] += options[:top_increment]
226 options[:top] += options[:top_increment]
227 @number_of_rows += 1
227 @number_of_rows += 1
228 break if abort?
228 break if abort?
229 end
229 end
230
230
231 options[:indent] -= (options[:indent_increment] * @issue_ancestors.size)
231 options[:indent] -= (options[:indent_increment] * @issue_ancestors.size)
232 end
232 end
233
233
234 def render_version(project, version, options={})
234 def render_version(project, version, options={})
235 # Version header
235 # Version header
236 subject_for_version(version, options) unless options[:only] == :lines
236 subject_for_version(version, options) unless options[:only] == :lines
237 line_for_version(version, options) unless options[:only] == :subjects
237 line_for_version(version, options) unless options[:only] == :subjects
238
238
239 options[:top] += options[:top_increment]
239 options[:top] += options[:top_increment]
240 @number_of_rows += 1
240 @number_of_rows += 1
241 return if abort?
241 return if abort?
242
242
243 issues = version_issues(project, version)
243 issues = version_issues(project, version)
244 if issues
244 if issues
245 sort_issues!(issues)
245 sort_issues!(issues)
246 # Indent issues
246 # Indent issues
247 options[:indent] += options[:indent_increment]
247 options[:indent] += options[:indent_increment]
248 render_issues(issues, options)
248 render_issues(issues, options)
249 options[:indent] -= options[:indent_increment]
249 options[:indent] -= options[:indent_increment]
250 end
250 end
251 end
251 end
252
252
253 def render_end(options={})
253 def render_end(options={})
254 case options[:format]
254 case options[:format]
255 when :pdf
255 when :pdf
256 options[:pdf].Line(15, options[:top], PDF::TotalWidth, options[:top])
256 options[:pdf].Line(15, options[:top], PDF::TotalWidth, options[:top])
257 end
257 end
258 end
258 end
259
259
260 def subject_for_project(project, options)
260 def subject_for_project(project, options)
261 case options[:format]
261 case options[:format]
262 when :html
262 when :html
263 subject = "<span class='icon icon-projects #{project.overdue? ? 'project-overdue' : ''}'>"
263 subject = "<span class='icon icon-projects #{project.overdue? ? 'project-overdue' : ''}'>"
264 subject << view.link_to_project(project)
264 subject << view.link_to_project(project)
265 subject << '</span>'
265 subject << '</span>'
266 html_subject(options, subject, :css => "project-name")
266 html_subject(options, subject, :css => "project-name")
267 when :image
267 when :image
268 image_subject(options, project.name)
268 image_subject(options, project.name)
269 when :pdf
269 when :pdf
270 pdf_new_page?(options)
270 pdf_new_page?(options)
271 pdf_subject(options, project.name)
271 pdf_subject(options, project.name)
272 end
272 end
273 end
273 end
274
274
275 def line_for_project(project, options)
275 def line_for_project(project, options)
276 # Skip versions that don't have a start_date or due date
276 # Skip versions that don't have a start_date or due date
277 if project.is_a?(Project) && project.start_date && project.due_date
277 if project.is_a?(Project) && project.start_date && project.due_date
278 options[:zoom] ||= 1
278 options[:zoom] ||= 1
279 options[:g_width] ||= (self.date_to - self.date_from + 1) * options[:zoom]
279 options[:g_width] ||= (self.date_to - self.date_from + 1) * options[:zoom]
280
280
281 coords = coordinates(project.start_date, project.due_date, nil, options[:zoom])
281 coords = coordinates(project.start_date, project.due_date, nil, options[:zoom])
282 label = h(project)
282 label = h(project)
283
283
284 case options[:format]
284 case options[:format]
285 when :html
285 when :html
286 html_task(options, coords, :css => "project task", :label => label, :markers => true)
286 html_task(options, coords, :css => "project task", :label => label, :markers => true)
287 when :image
287 when :image
288 image_task(options, coords, :label => label, :markers => true, :height => 3)
288 image_task(options, coords, :label => label, :markers => true, :height => 3)
289 when :pdf
289 when :pdf
290 pdf_task(options, coords, :label => label, :markers => true, :height => 0.8)
290 pdf_task(options, coords, :label => label, :markers => true, :height => 0.8)
291 end
291 end
292 else
292 else
293 ActiveRecord::Base.logger.debug "Gantt#line_for_project was not given a project with a start_date"
293 ActiveRecord::Base.logger.debug "Gantt#line_for_project was not given a project with a start_date"
294 ''
294 ''
295 end
295 end
296 end
296 end
297
297
298 def subject_for_version(version, options)
298 def subject_for_version(version, options)
299 case options[:format]
299 case options[:format]
300 when :html
300 when :html
301 subject = "<span class='icon icon-package #{version.behind_schedule? ? 'version-behind-schedule' : ''} #{version.overdue? ? 'version-overdue' : ''}'>"
301 subject = "<span class='icon icon-package #{version.behind_schedule? ? 'version-behind-schedule' : ''} #{version.overdue? ? 'version-overdue' : ''}'>"
302 subject << view.link_to_version(version)
302 subject << view.link_to_version(version)
303 subject << '</span>'
303 subject << '</span>'
304 html_subject(options, subject, :css => "version-name")
304 html_subject(options, subject, :css => "version-name")
305 when :image
305 when :image
306 image_subject(options, version.to_s_with_project)
306 image_subject(options, version.to_s_with_project)
307 when :pdf
307 when :pdf
308 pdf_new_page?(options)
308 pdf_new_page?(options)
309 pdf_subject(options, version.to_s_with_project)
309 pdf_subject(options, version.to_s_with_project)
310 end
310 end
311 end
311 end
312
312
313 def line_for_version(version, options)
313 def line_for_version(version, options)
314 # Skip versions that don't have a start_date
314 # Skip versions that don't have a start_date
315 if version.is_a?(Version) && version.start_date && version.due_date
315 if version.is_a?(Version) && version.start_date && version.due_date
316 options[:zoom] ||= 1
316 options[:zoom] ||= 1
317 options[:g_width] ||= (self.date_to - self.date_from + 1) * options[:zoom]
317 options[:g_width] ||= (self.date_to - self.date_from + 1) * options[:zoom]
318
318
319 coords = coordinates(version.start_date, version.due_date, version.completed_pourcent, options[:zoom])
319 coords = coordinates(version.start_date, version.due_date, version.completed_pourcent, options[:zoom])
320 label = "#{h version } #{h version.completed_pourcent.to_i.to_s}%"
320 label = "#{h version } #{h version.completed_pourcent.to_i.to_s}%"
321 label = h("#{version.project} -") + label unless @project && @project == version.project
321 label = h("#{version.project} -") + label unless @project && @project == version.project
322
322
323 case options[:format]
323 case options[:format]
324 when :html
324 when :html
325 html_task(options, coords, :css => "version task", :label => label, :markers => true)
325 html_task(options, coords, :css => "version task", :label => label, :markers => true)
326 when :image
326 when :image
327 image_task(options, coords, :label => label, :markers => true, :height => 3)
327 image_task(options, coords, :label => label, :markers => true, :height => 3)
328 when :pdf
328 when :pdf
329 pdf_task(options, coords, :label => label, :markers => true, :height => 0.8)
329 pdf_task(options, coords, :label => label, :markers => true, :height => 0.8)
330 end
330 end
331 else
331 else
332 ActiveRecord::Base.logger.debug "Gantt#line_for_version was not given a version with a start_date"
332 ActiveRecord::Base.logger.debug "Gantt#line_for_version was not given a version with a start_date"
333 ''
333 ''
334 end
334 end
335 end
335 end
336
336
337 def subject_for_issue(issue, options)
337 def subject_for_issue(issue, options)
338 while @issue_ancestors.any? && !issue.is_descendant_of?(@issue_ancestors.last)
338 while @issue_ancestors.any? && !issue.is_descendant_of?(@issue_ancestors.last)
339 @issue_ancestors.pop
339 @issue_ancestors.pop
340 options[:indent] -= options[:indent_increment]
340 options[:indent] -= options[:indent_increment]
341 end
341 end
342
342
343 output = case options[:format]
343 output = case options[:format]
344 when :html
344 when :html
345 css_classes = ''
345 css_classes = ''
346 css_classes << ' issue-overdue' if issue.overdue?
346 css_classes << ' issue-overdue' if issue.overdue?
347 css_classes << ' issue-behind-schedule' if issue.behind_schedule?
347 css_classes << ' issue-behind-schedule' if issue.behind_schedule?
348 css_classes << ' icon icon-issue' unless Setting.gravatar_enabled? && issue.assigned_to
348 css_classes << ' icon icon-issue' unless Setting.gravatar_enabled? && issue.assigned_to
349
349
350 subject = "<span class='#{css_classes}'>"
350 subject = "<span class='#{css_classes}'>"
351 if issue.assigned_to.present?
351 if issue.assigned_to.present?
352 assigned_string = l(:field_assigned_to) + ": " + issue.assigned_to.name
352 assigned_string = l(:field_assigned_to) + ": " + issue.assigned_to.name
353 subject << view.avatar(issue.assigned_to, :class => 'gravatar icon-gravatar', :size => 10, :title => assigned_string).to_s
353 subject << view.avatar(issue.assigned_to, :class => 'gravatar icon-gravatar', :size => 10, :title => assigned_string).to_s
354 end
354 end
355 subject << view.link_to_issue(issue)
355 subject << view.link_to_issue(issue)
356 subject << '</span>'
356 subject << '</span>'
357 html_subject(options, subject, :css => "issue-subject", :title => issue.subject) + "\n"
357 html_subject(options, subject, :css => "issue-subject", :title => issue.subject) + "\n"
358 when :image
358 when :image
359 image_subject(options, issue.subject)
359 image_subject(options, issue.subject)
360 when :pdf
360 when :pdf
361 pdf_new_page?(options)
361 pdf_new_page?(options)
362 pdf_subject(options, issue.subject)
362 pdf_subject(options, issue.subject)
363 end
363 end
364
364
365 unless issue.leaf?
365 unless issue.leaf?
366 @issue_ancestors << issue
366 @issue_ancestors << issue
367 options[:indent] += options[:indent_increment]
367 options[:indent] += options[:indent_increment]
368 end
368 end
369
369
370 output
370 output
371 end
371 end
372
372
373 def line_for_issue(issue, options)
373 def line_for_issue(issue, options)
374 # Skip issues that don't have a due_before (due_date or version's due_date)
374 # Skip issues that don't have a due_before (due_date or version's due_date)
375 if issue.is_a?(Issue) && issue.due_before
375 if issue.is_a?(Issue) && issue.due_before
376 coords = coordinates(issue.start_date, issue.due_before, issue.done_ratio, options[:zoom])
376 coords = coordinates(issue.start_date, issue.due_before, issue.done_ratio, options[:zoom])
377 label = "#{ issue.status.name } #{ issue.done_ratio }%"
377 label = "#{ issue.status.name } #{ issue.done_ratio }%"
378
378
379 case options[:format]
379 case options[:format]
380 when :html
380 when :html
381 html_task(options, coords, :css => "task " + (issue.leaf? ? 'leaf' : 'parent'), :label => label, :issue => issue, :markers => !issue.leaf?)
381 html_task(options, coords, :css => "task " + (issue.leaf? ? 'leaf' : 'parent'), :label => label, :issue => issue, :markers => !issue.leaf?)
382 when :image
382 when :image
383 image_task(options, coords, :label => label)
383 image_task(options, coords, :label => label)
384 when :pdf
384 when :pdf
385 pdf_task(options, coords, :label => label)
385 pdf_task(options, coords, :label => label)
386 end
386 end
387 else
387 else
388 ActiveRecord::Base.logger.debug "GanttHelper#line_for_issue was not given an issue with a due_before"
388 ActiveRecord::Base.logger.debug "GanttHelper#line_for_issue was not given an issue with a due_before"
389 ''
389 ''
390 end
390 end
391 end
391 end
392
392
393 # Generates a gantt image
393 # Generates a gantt image
394 # Only defined if RMagick is avalaible
394 # Only defined if RMagick is avalaible
395 def to_image(format='PNG')
395 def to_image(format='PNG')
396 date_to = (@date_from >> @months)-1
396 date_to = (@date_from >> @months)-1
397 show_weeks = @zoom > 1
397 show_weeks = @zoom > 1
398 show_days = @zoom > 2
398 show_days = @zoom > 2
399
399
400 subject_width = 400
400 subject_width = 400
401 header_heigth = 18
401 header_heigth = 18
402 # width of one day in pixels
402 # width of one day in pixels
403 zoom = @zoom*2
403 zoom = @zoom*2
404 g_width = (@date_to - @date_from + 1)*zoom
404 g_width = (@date_to - @date_from + 1)*zoom
405 g_height = 20 * number_of_rows + 30
405 g_height = 20 * number_of_rows + 30
406 headers_heigth = (show_weeks ? 2*header_heigth : header_heigth)
406 headers_heigth = (show_weeks ? 2*header_heigth : header_heigth)
407 height = g_height + headers_heigth
407 height = g_height + headers_heigth
408
408
409 imgl = Magick::ImageList.new
409 imgl = Magick::ImageList.new
410 imgl.new_image(subject_width+g_width+1, height)
410 imgl.new_image(subject_width+g_width+1, height)
411 gc = Magick::Draw.new
411 gc = Magick::Draw.new
412
412
413 # Subjects
413 # Subjects
414 gc.stroke('transparent')
414 gc.stroke('transparent')
415 subjects(:image => gc, :top => (headers_heigth + 20), :indent => 4, :format => :image)
415 subjects(:image => gc, :top => (headers_heigth + 20), :indent => 4, :format => :image)
416
416
417 # Months headers
417 # Months headers
418 month_f = @date_from
418 month_f = @date_from
419 left = subject_width
419 left = subject_width
420 @months.times do
420 @months.times do
421 width = ((month_f >> 1) - month_f) * zoom
421 width = ((month_f >> 1) - month_f) * zoom
422 gc.fill('white')
422 gc.fill('white')
423 gc.stroke('grey')
423 gc.stroke('grey')
424 gc.stroke_width(1)
424 gc.stroke_width(1)
425 gc.rectangle(left, 0, left + width, height)
425 gc.rectangle(left, 0, left + width, height)
426 gc.fill('black')
426 gc.fill('black')
427 gc.stroke('transparent')
427 gc.stroke('transparent')
428 gc.stroke_width(1)
428 gc.stroke_width(1)
429 gc.text(left.round + 8, 14, "#{month_f.year}-#{month_f.month}")
429 gc.text(left.round + 8, 14, "#{month_f.year}-#{month_f.month}")
430 left = left + width
430 left = left + width
431 month_f = month_f >> 1
431 month_f = month_f >> 1
432 end
432 end
433
433
434 # Weeks headers
434 # Weeks headers
435 if show_weeks
435 if show_weeks
436 left = subject_width
436 left = subject_width
437 height = header_heigth
437 height = header_heigth
438 if @date_from.cwday == 1
438 if @date_from.cwday == 1
439 # date_from is monday
439 # date_from is monday
440 week_f = date_from
440 week_f = date_from
441 else
441 else
442 # find next monday after date_from
442 # find next monday after date_from
443 week_f = @date_from + (7 - @date_from.cwday + 1)
443 week_f = @date_from + (7 - @date_from.cwday + 1)
444 width = (7 - @date_from.cwday + 1) * zoom
444 width = (7 - @date_from.cwday + 1) * zoom
445 gc.fill('white')
445 gc.fill('white')
446 gc.stroke('grey')
446 gc.stroke('grey')
447 gc.stroke_width(1)
447 gc.stroke_width(1)
448 gc.rectangle(left, header_heigth, left + width, 2*header_heigth + g_height-1)
448 gc.rectangle(left, header_heigth, left + width, 2*header_heigth + g_height-1)
449 left = left + width
449 left = left + width
450 end
450 end
451 while week_f <= date_to
451 while week_f <= date_to
452 width = (week_f + 6 <= date_to) ? 7 * zoom : (date_to - week_f + 1) * zoom
452 width = (week_f + 6 <= date_to) ? 7 * zoom : (date_to - week_f + 1) * zoom
453 gc.fill('white')
453 gc.fill('white')
454 gc.stroke('grey')
454 gc.stroke('grey')
455 gc.stroke_width(1)
455 gc.stroke_width(1)
456 gc.rectangle(left.round, header_heigth, left.round + width, 2*header_heigth + g_height-1)
456 gc.rectangle(left.round, header_heigth, left.round + width, 2*header_heigth + g_height-1)
457 gc.fill('black')
457 gc.fill('black')
458 gc.stroke('transparent')
458 gc.stroke('transparent')
459 gc.stroke_width(1)
459 gc.stroke_width(1)
460 gc.text(left.round + 2, header_heigth + 14, week_f.cweek.to_s)
460 gc.text(left.round + 2, header_heigth + 14, week_f.cweek.to_s)
461 left = left + width
461 left = left + width
462 week_f = week_f+7
462 week_f = week_f+7
463 end
463 end
464 end
464 end
465
465
466 # Days details (week-end in grey)
466 # Days details (week-end in grey)
467 if show_days
467 if show_days
468 left = subject_width
468 left = subject_width
469 height = g_height + header_heigth - 1
469 height = g_height + header_heigth - 1
470 wday = @date_from.cwday
470 wday = @date_from.cwday
471 (date_to - @date_from + 1).to_i.times do
471 (date_to - @date_from + 1).to_i.times do
472 width = zoom
472 width = zoom
473 gc.fill(wday == 6 || wday == 7 ? '#eee' : 'white')
473 gc.fill(wday == 6 || wday == 7 ? '#eee' : 'white')
474 gc.stroke('#ddd')
474 gc.stroke('#ddd')
475 gc.stroke_width(1)
475 gc.stroke_width(1)
476 gc.rectangle(left, 2*header_heigth, left + width, 2*header_heigth + g_height-1)
476 gc.rectangle(left, 2*header_heigth, left + width, 2*header_heigth + g_height-1)
477 left = left + width
477 left = left + width
478 wday = wday + 1
478 wday = wday + 1
479 wday = 1 if wday > 7
479 wday = 1 if wday > 7
480 end
480 end
481 end
481 end
482
482
483 # border
483 # border
484 gc.fill('transparent')
484 gc.fill('transparent')
485 gc.stroke('grey')
485 gc.stroke('grey')
486 gc.stroke_width(1)
486 gc.stroke_width(1)
487 gc.rectangle(0, 0, subject_width+g_width, headers_heigth)
487 gc.rectangle(0, 0, subject_width+g_width, headers_heigth)
488 gc.stroke('black')
488 gc.stroke('black')
489 gc.rectangle(0, 0, subject_width+g_width, g_height+ headers_heigth-1)
489 gc.rectangle(0, 0, subject_width+g_width, g_height+ headers_heigth-1)
490
490
491 # content
491 # content
492 top = headers_heigth + 20
492 top = headers_heigth + 20
493
493
494 gc.stroke('transparent')
494 gc.stroke('transparent')
495 lines(:image => gc, :top => top, :zoom => zoom, :subject_width => subject_width, :format => :image)
495 lines(:image => gc, :top => top, :zoom => zoom, :subject_width => subject_width, :format => :image)
496
496
497 # today red line
497 # today red line
498 if Date.today >= @date_from and Date.today <= date_to
498 if Date.today >= @date_from and Date.today <= date_to
499 gc.stroke('red')
499 gc.stroke('red')
500 x = (Date.today-@date_from+1)*zoom + subject_width
500 x = (Date.today-@date_from+1)*zoom + subject_width
501 gc.line(x, headers_heigth, x, headers_heigth + g_height-1)
501 gc.line(x, headers_heigth, x, headers_heigth + g_height-1)
502 end
502 end
503
503
504 gc.draw(imgl)
504 gc.draw(imgl)
505 imgl.format = format
505 imgl.format = format
506 imgl.to_blob
506 imgl.to_blob
507 end if Object.const_defined?(:Magick)
507 end if Object.const_defined?(:Magick)
508
508
509 def to_pdf
509 def to_pdf
510 if Redmine::Platform.mswin? ||
511 ( current_language.to_s.downcase == 'ko' ||
512 current_language.to_s.downcase == 'ja' ||
513 current_language.to_s.downcase == 'zh' ||
514 current_language.to_s.downcase == 'zh-tw' ||
515 current_language.to_s.downcase == 'th' )
510 pdf = ::Redmine::Export::PDF::IFPDF.new(current_language)
516 pdf = ::Redmine::Export::PDF::IFPDF.new(current_language)
517 else
518 pdf = ::Redmine::Export::PDF::ITCPDF.new(current_language)
519 end
511 pdf.SetTitle("#{l(:label_gantt)} #{project}")
520 pdf.SetTitle("#{l(:label_gantt)} #{project}")
512 pdf.alias_nb_pages
521 pdf.alias_nb_pages
513 pdf.footer_date = format_date(Date.today)
522 pdf.footer_date = format_date(Date.today)
514 pdf.AddPage("L")
523 pdf.AddPage("L")
515 pdf.SetFontStyle('B',12)
524 pdf.SetFontStyle('B',12)
516 pdf.SetX(15)
525 pdf.SetX(15)
517 pdf.RDMCell(PDF::LeftPaneWidth, 20, project.to_s)
526 pdf.RDMCell(PDF::LeftPaneWidth, 20, project.to_s)
518 pdf.Ln
527 pdf.Ln
519 pdf.SetFontStyle('B',9)
528 pdf.SetFontStyle('B',9)
520
529
521 subject_width = PDF::LeftPaneWidth
530 subject_width = PDF::LeftPaneWidth
522 header_heigth = 5
531 header_heigth = 5
523
532
524 headers_heigth = header_heigth
533 headers_heigth = header_heigth
525 show_weeks = false
534 show_weeks = false
526 show_days = false
535 show_days = false
527
536
528 if self.months < 7
537 if self.months < 7
529 show_weeks = true
538 show_weeks = true
530 headers_heigth = 2*header_heigth
539 headers_heigth = 2*header_heigth
531 if self.months < 3
540 if self.months < 3
532 show_days = true
541 show_days = true
533 headers_heigth = 3*header_heigth
542 headers_heigth = 3*header_heigth
534 end
543 end
535 end
544 end
536
545
537 g_width = PDF.right_pane_width
546 g_width = PDF.right_pane_width
538 zoom = (g_width) / (self.date_to - self.date_from + 1)
547 zoom = (g_width) / (self.date_to - self.date_from + 1)
539 g_height = 120
548 g_height = 120
540 t_height = g_height + headers_heigth
549 t_height = g_height + headers_heigth
541
550
542 y_start = pdf.GetY
551 y_start = pdf.GetY
543
552
544 # Months headers
553 # Months headers
545 month_f = self.date_from
554 month_f = self.date_from
546 left = subject_width
555 left = subject_width
547 height = header_heigth
556 height = header_heigth
548 self.months.times do
557 self.months.times do
549 width = ((month_f >> 1) - month_f) * zoom
558 width = ((month_f >> 1) - month_f) * zoom
550 pdf.SetY(y_start)
559 pdf.SetY(y_start)
551 pdf.SetX(left)
560 pdf.SetX(left)
552 pdf.RDMCell(width, height, "#{month_f.year}-#{month_f.month}", "LTR", 0, "C")
561 pdf.RDMCell(width, height, "#{month_f.year}-#{month_f.month}", "LTR", 0, "C")
553 left = left + width
562 left = left + width
554 month_f = month_f >> 1
563 month_f = month_f >> 1
555 end
564 end
556
565
557 # Weeks headers
566 # Weeks headers
558 if show_weeks
567 if show_weeks
559 left = subject_width
568 left = subject_width
560 height = header_heigth
569 height = header_heigth
561 if self.date_from.cwday == 1
570 if self.date_from.cwday == 1
562 # self.date_from is monday
571 # self.date_from is monday
563 week_f = self.date_from
572 week_f = self.date_from
564 else
573 else
565 # find next monday after self.date_from
574 # find next monday after self.date_from
566 week_f = self.date_from + (7 - self.date_from.cwday + 1)
575 week_f = self.date_from + (7 - self.date_from.cwday + 1)
567 width = (7 - self.date_from.cwday + 1) * zoom-1
576 width = (7 - self.date_from.cwday + 1) * zoom-1
568 pdf.SetY(y_start + header_heigth)
577 pdf.SetY(y_start + header_heigth)
569 pdf.SetX(left)
578 pdf.SetX(left)
570 pdf.RDMCell(width + 1, height, "", "LTR")
579 pdf.RDMCell(width + 1, height, "", "LTR")
571 left = left + width+1
580 left = left + width+1
572 end
581 end
573 while week_f <= self.date_to
582 while week_f <= self.date_to
574 width = (week_f + 6 <= self.date_to) ? 7 * zoom : (self.date_to - week_f + 1) * zoom
583 width = (week_f + 6 <= self.date_to) ? 7 * zoom : (self.date_to - week_f + 1) * zoom
575 pdf.SetY(y_start + header_heigth)
584 pdf.SetY(y_start + header_heigth)
576 pdf.SetX(left)
585 pdf.SetX(left)
577 pdf.RDMCell(width, height, (width >= 5 ? week_f.cweek.to_s : ""), "LTR", 0, "C")
586 pdf.RDMCell(width, height, (width >= 5 ? week_f.cweek.to_s : ""), "LTR", 0, "C")
578 left = left + width
587 left = left + width
579 week_f = week_f+7
588 week_f = week_f+7
580 end
589 end
581 end
590 end
582
591
583 # Days headers
592 # Days headers
584 if show_days
593 if show_days
585 left = subject_width
594 left = subject_width
586 height = header_heigth
595 height = header_heigth
587 wday = self.date_from.cwday
596 wday = self.date_from.cwday
588 pdf.SetFontStyle('B',7)
597 pdf.SetFontStyle('B',7)
589 (self.date_to - self.date_from + 1).to_i.times do
598 (self.date_to - self.date_from + 1).to_i.times do
590 width = zoom
599 width = zoom
591 pdf.SetY(y_start + 2 * header_heigth)
600 pdf.SetY(y_start + 2 * header_heigth)
592 pdf.SetX(left)
601 pdf.SetX(left)
593 pdf.RDMCell(width, height, day_name(wday).first, "LTR", 0, "C")
602 pdf.RDMCell(width, height, day_name(wday).first, "LTR", 0, "C")
594 left = left + width
603 left = left + width
595 wday = wday + 1
604 wday = wday + 1
596 wday = 1 if wday > 7
605 wday = 1 if wday > 7
597 end
606 end
598 end
607 end
599
608
600 pdf.SetY(y_start)
609 pdf.SetY(y_start)
601 pdf.SetX(15)
610 pdf.SetX(15)
602 pdf.RDMCell(subject_width+g_width-15, headers_heigth, "", 1)
611 pdf.RDMCell(subject_width+g_width-15, headers_heigth, "", 1)
603
612
604 # Tasks
613 # Tasks
605 top = headers_heigth + y_start
614 top = headers_heigth + y_start
606 options = {
615 options = {
607 :top => top,
616 :top => top,
608 :zoom => zoom,
617 :zoom => zoom,
609 :subject_width => subject_width,
618 :subject_width => subject_width,
610 :g_width => g_width,
619 :g_width => g_width,
611 :indent => 0,
620 :indent => 0,
612 :indent_increment => 5,
621 :indent_increment => 5,
613 :top_increment => 5,
622 :top_increment => 5,
614 :format => :pdf,
623 :format => :pdf,
615 :pdf => pdf
624 :pdf => pdf
616 }
625 }
617 render(options)
626 render(options)
618 pdf.Output
627 pdf.Output
619 end
628 end
620
629
621 private
630 private
622
631
623 def coordinates(start_date, end_date, progress, zoom=nil)
632 def coordinates(start_date, end_date, progress, zoom=nil)
624 zoom ||= @zoom
633 zoom ||= @zoom
625
634
626 coords = {}
635 coords = {}
627 if start_date && end_date && start_date < self.date_to && end_date > self.date_from
636 if start_date && end_date && start_date < self.date_to && end_date > self.date_from
628 if start_date > self.date_from
637 if start_date > self.date_from
629 coords[:start] = start_date - self.date_from
638 coords[:start] = start_date - self.date_from
630 coords[:bar_start] = start_date - self.date_from
639 coords[:bar_start] = start_date - self.date_from
631 else
640 else
632 coords[:bar_start] = 0
641 coords[:bar_start] = 0
633 end
642 end
634 if end_date < self.date_to
643 if end_date < self.date_to
635 coords[:end] = end_date - self.date_from
644 coords[:end] = end_date - self.date_from
636 coords[:bar_end] = end_date - self.date_from + 1
645 coords[:bar_end] = end_date - self.date_from + 1
637 else
646 else
638 coords[:bar_end] = self.date_to - self.date_from + 1
647 coords[:bar_end] = self.date_to - self.date_from + 1
639 end
648 end
640
649
641 if progress
650 if progress
642 progress_date = start_date + (end_date - start_date + 1) * (progress / 100.0)
651 progress_date = start_date + (end_date - start_date + 1) * (progress / 100.0)
643 if progress_date > self.date_from && progress_date > start_date
652 if progress_date > self.date_from && progress_date > start_date
644 if progress_date < self.date_to
653 if progress_date < self.date_to
645 coords[:bar_progress_end] = progress_date - self.date_from
654 coords[:bar_progress_end] = progress_date - self.date_from
646 else
655 else
647 coords[:bar_progress_end] = self.date_to - self.date_from + 1
656 coords[:bar_progress_end] = self.date_to - self.date_from + 1
648 end
657 end
649 end
658 end
650
659
651 if progress_date < Date.today
660 if progress_date < Date.today
652 late_date = [Date.today, end_date].min
661 late_date = [Date.today, end_date].min
653 if late_date > self.date_from && late_date > start_date
662 if late_date > self.date_from && late_date > start_date
654 if late_date < self.date_to
663 if late_date < self.date_to
655 coords[:bar_late_end] = late_date - self.date_from + 1
664 coords[:bar_late_end] = late_date - self.date_from + 1
656 else
665 else
657 coords[:bar_late_end] = self.date_to - self.date_from + 1
666 coords[:bar_late_end] = self.date_to - self.date_from + 1
658 end
667 end
659 end
668 end
660 end
669 end
661 end
670 end
662 end
671 end
663
672
664 # Transforms dates into pixels witdh
673 # Transforms dates into pixels witdh
665 coords.keys.each do |key|
674 coords.keys.each do |key|
666 coords[key] = (coords[key] * zoom).floor
675 coords[key] = (coords[key] * zoom).floor
667 end
676 end
668 coords
677 coords
669 end
678 end
670
679
671 # Sorts a collection of issues by start_date, due_date, id for gantt rendering
680 # Sorts a collection of issues by start_date, due_date, id for gantt rendering
672 def sort_issues!(issues)
681 def sort_issues!(issues)
673 issues.sort! { |a, b| gantt_issue_compare(a, b, issues) }
682 issues.sort! { |a, b| gantt_issue_compare(a, b, issues) }
674 end
683 end
675
684
676 # TODO: top level issues should be sorted by start date
685 # TODO: top level issues should be sorted by start date
677 def gantt_issue_compare(x, y, issues)
686 def gantt_issue_compare(x, y, issues)
678 if x.root_id == y.root_id
687 if x.root_id == y.root_id
679 x.lft <=> y.lft
688 x.lft <=> y.lft
680 else
689 else
681 x.root_id <=> y.root_id
690 x.root_id <=> y.root_id
682 end
691 end
683 end
692 end
684
693
685 def current_limit
694 def current_limit
686 if @max_rows
695 if @max_rows
687 @max_rows - @number_of_rows
696 @max_rows - @number_of_rows
688 else
697 else
689 nil
698 nil
690 end
699 end
691 end
700 end
692
701
693 def abort?
702 def abort?
694 if @max_rows && @number_of_rows >= @max_rows
703 if @max_rows && @number_of_rows >= @max_rows
695 @truncated = true
704 @truncated = true
696 end
705 end
697 end
706 end
698
707
699 def pdf_new_page?(options)
708 def pdf_new_page?(options)
700 if options[:top] > 180
709 if options[:top] > 180
701 options[:pdf].Line(15, options[:top], PDF::TotalWidth, options[:top])
710 options[:pdf].Line(15, options[:top], PDF::TotalWidth, options[:top])
702 options[:pdf].AddPage("L")
711 options[:pdf].AddPage("L")
703 options[:top] = 15
712 options[:top] = 15
704 options[:pdf].Line(15, options[:top] - 0.1, PDF::TotalWidth, options[:top] - 0.1)
713 options[:pdf].Line(15, options[:top] - 0.1, PDF::TotalWidth, options[:top] - 0.1)
705 end
714 end
706 end
715 end
707
716
708 def html_subject(params, subject, options={})
717 def html_subject(params, subject, options={})
709 style = "position: absolute;top:#{params[:top]}px;left:#{params[:indent]}px;"
718 style = "position: absolute;top:#{params[:top]}px;left:#{params[:indent]}px;"
710 style << "width:#{params[:subject_width] - params[:indent]}px;" if params[:subject_width]
719 style << "width:#{params[:subject_width] - params[:indent]}px;" if params[:subject_width]
711
720
712 output = view.content_tag 'div', subject, :class => options[:css], :style => style, :title => options[:title]
721 output = view.content_tag 'div', subject, :class => options[:css], :style => style, :title => options[:title]
713 @subjects << output
722 @subjects << output
714 output
723 output
715 end
724 end
716
725
717 def pdf_subject(params, subject, options={})
726 def pdf_subject(params, subject, options={})
718 params[:pdf].SetY(params[:top])
727 params[:pdf].SetY(params[:top])
719 params[:pdf].SetX(15)
728 params[:pdf].SetX(15)
720
729
721 char_limit = PDF::MaxCharactorsForSubject - params[:indent]
730 char_limit = PDF::MaxCharactorsForSubject - params[:indent]
722 params[:pdf].RDMCell(params[:subject_width]-15, 5, (" " * params[:indent]) + subject.to_s.sub(/^(.{#{char_limit}}[^\s]*\s).*$/, '\1 (...)'), "LR")
731 params[:pdf].RDMCell(params[:subject_width]-15, 5, (" " * params[:indent]) + subject.to_s.sub(/^(.{#{char_limit}}[^\s]*\s).*$/, '\1 (...)'), "LR")
723
732
724 params[:pdf].SetY(params[:top])
733 params[:pdf].SetY(params[:top])
725 params[:pdf].SetX(params[:subject_width])
734 params[:pdf].SetX(params[:subject_width])
726 params[:pdf].RDMCell(params[:g_width], 5, "", "LR")
735 params[:pdf].RDMCell(params[:g_width], 5, "", "LR")
727 end
736 end
728
737
729 def image_subject(params, subject, options={})
738 def image_subject(params, subject, options={})
730 params[:image].fill('black')
739 params[:image].fill('black')
731 params[:image].stroke('transparent')
740 params[:image].stroke('transparent')
732 params[:image].stroke_width(1)
741 params[:image].stroke_width(1)
733 params[:image].text(params[:indent], params[:top] + 2, subject)
742 params[:image].text(params[:indent], params[:top] + 2, subject)
734 end
743 end
735
744
736 def html_task(params, coords, options={})
745 def html_task(params, coords, options={})
737 output = ''
746 output = ''
738 # Renders the task bar, with progress and late
747 # Renders the task bar, with progress and late
739 if coords[:bar_start] && coords[:bar_end]
748 if coords[:bar_start] && coords[:bar_end]
740 output << "<div style='top:#{ params[:top] }px;left:#{ coords[:bar_start] }px;width:#{ coords[:bar_end] - coords[:bar_start] - 2}px;' class='#{options[:css]} task_todo'>&nbsp;</div>"
749 output << "<div style='top:#{ params[:top] }px;left:#{ coords[:bar_start] }px;width:#{ coords[:bar_end] - coords[:bar_start] - 2}px;' class='#{options[:css]} task_todo'>&nbsp;</div>"
741
750
742 if coords[:bar_late_end]
751 if coords[:bar_late_end]
743 output << "<div style='top:#{ params[:top] }px;left:#{ coords[:bar_start] }px;width:#{ coords[:bar_late_end] - coords[:bar_start] - 2}px;' class='#{options[:css]} task_late'>&nbsp;</div>"
752 output << "<div style='top:#{ params[:top] }px;left:#{ coords[:bar_start] }px;width:#{ coords[:bar_late_end] - coords[:bar_start] - 2}px;' class='#{options[:css]} task_late'>&nbsp;</div>"
744 end
753 end
745 if coords[:bar_progress_end]
754 if coords[:bar_progress_end]
746 output << "<div style='top:#{ params[:top] }px;left:#{ coords[:bar_start] }px;width:#{ coords[:bar_progress_end] - coords[:bar_start] - 2}px;' class='#{options[:css]} task_done'>&nbsp;</div>"
755 output << "<div style='top:#{ params[:top] }px;left:#{ coords[:bar_start] }px;width:#{ coords[:bar_progress_end] - coords[:bar_start] - 2}px;' class='#{options[:css]} task_done'>&nbsp;</div>"
747 end
756 end
748 end
757 end
749 # Renders the markers
758 # Renders the markers
750 if options[:markers]
759 if options[:markers]
751 if coords[:start]
760 if coords[:start]
752 output << "<div style='top:#{ params[:top] }px;left:#{ coords[:start] }px;width:15px;' class='#{options[:css]} marker starting'>&nbsp;</div>"
761 output << "<div style='top:#{ params[:top] }px;left:#{ coords[:start] }px;width:15px;' class='#{options[:css]} marker starting'>&nbsp;</div>"
753 end
762 end
754 if coords[:end]
763 if coords[:end]
755 output << "<div style='top:#{ params[:top] }px;left:#{ coords[:end] + params[:zoom] }px;width:15px;' class='#{options[:css]} marker ending'>&nbsp;</div>"
764 output << "<div style='top:#{ params[:top] }px;left:#{ coords[:end] + params[:zoom] }px;width:15px;' class='#{options[:css]} marker ending'>&nbsp;</div>"
756 end
765 end
757 end
766 end
758 # Renders the label on the right
767 # Renders the label on the right
759 if options[:label]
768 if options[:label]
760 output << "<div style='top:#{ params[:top] }px;left:#{ (coords[:bar_end] || 0) + 8 }px;' class='#{options[:css]} label'>"
769 output << "<div style='top:#{ params[:top] }px;left:#{ (coords[:bar_end] || 0) + 8 }px;' class='#{options[:css]} label'>"
761 output << options[:label]
770 output << options[:label]
762 output << "</div>"
771 output << "</div>"
763 end
772 end
764 # Renders the tooltip
773 # Renders the tooltip
765 if options[:issue] && coords[:bar_start] && coords[:bar_end]
774 if options[:issue] && coords[:bar_start] && coords[:bar_end]
766 output << "<div class='tooltip' style='position: absolute;top:#{ params[:top] }px;left:#{ coords[:bar_start] }px;width:#{ coords[:bar_end] - coords[:bar_start] }px;height:12px;'>"
775 output << "<div class='tooltip' style='position: absolute;top:#{ params[:top] }px;left:#{ coords[:bar_start] }px;width:#{ coords[:bar_end] - coords[:bar_start] }px;height:12px;'>"
767 output << '<span class="tip">'
776 output << '<span class="tip">'
768 output << view.render_issue_tooltip(options[:issue])
777 output << view.render_issue_tooltip(options[:issue])
769 output << "</span></div>"
778 output << "</span></div>"
770 end
779 end
771 @lines << output
780 @lines << output
772 output
781 output
773 end
782 end
774
783
775 def pdf_task(params, coords, options={})
784 def pdf_task(params, coords, options={})
776 height = options[:height] || 2
785 height = options[:height] || 2
777
786
778 # Renders the task bar, with progress and late
787 # Renders the task bar, with progress and late
779 if coords[:bar_start] && coords[:bar_end]
788 if coords[:bar_start] && coords[:bar_end]
780 params[:pdf].SetY(params[:top]+1.5)
789 params[:pdf].SetY(params[:top]+1.5)
781 params[:pdf].SetX(params[:subject_width] + coords[:bar_start])
790 params[:pdf].SetX(params[:subject_width] + coords[:bar_start])
782 params[:pdf].SetFillColor(200,200,200)
791 params[:pdf].SetFillColor(200,200,200)
783 params[:pdf].RDMCell(coords[:bar_end] - coords[:bar_start], height, "", 0, 0, "", 1)
792 params[:pdf].RDMCell(coords[:bar_end] - coords[:bar_start], height, "", 0, 0, "", 1)
784
793
785 if coords[:bar_late_end]
794 if coords[:bar_late_end]
786 params[:pdf].SetY(params[:top]+1.5)
795 params[:pdf].SetY(params[:top]+1.5)
787 params[:pdf].SetX(params[:subject_width] + coords[:bar_start])
796 params[:pdf].SetX(params[:subject_width] + coords[:bar_start])
788 params[:pdf].SetFillColor(255,100,100)
797 params[:pdf].SetFillColor(255,100,100)
789 params[:pdf].RDMCell(coords[:bar_late_end] - coords[:bar_start], height, "", 0, 0, "", 1)
798 params[:pdf].RDMCell(coords[:bar_late_end] - coords[:bar_start], height, "", 0, 0, "", 1)
790 end
799 end
791 if coords[:bar_progress_end]
800 if coords[:bar_progress_end]
792 params[:pdf].SetY(params[:top]+1.5)
801 params[:pdf].SetY(params[:top]+1.5)
793 params[:pdf].SetX(params[:subject_width] + coords[:bar_start])
802 params[:pdf].SetX(params[:subject_width] + coords[:bar_start])
794 params[:pdf].SetFillColor(90,200,90)
803 params[:pdf].SetFillColor(90,200,90)
795 params[:pdf].RDMCell(coords[:bar_progress_end] - coords[:bar_start], height, "", 0, 0, "", 1)
804 params[:pdf].RDMCell(coords[:bar_progress_end] - coords[:bar_start], height, "", 0, 0, "", 1)
796 end
805 end
797 end
806 end
798 # Renders the markers
807 # Renders the markers
799 if options[:markers]
808 if options[:markers]
800 if coords[:start]
809 if coords[:start]
801 params[:pdf].SetY(params[:top] + 1)
810 params[:pdf].SetY(params[:top] + 1)
802 params[:pdf].SetX(params[:subject_width] + coords[:start] - 1)
811 params[:pdf].SetX(params[:subject_width] + coords[:start] - 1)
803 params[:pdf].SetFillColor(50,50,200)
812 params[:pdf].SetFillColor(50,50,200)
804 params[:pdf].RDMCell(2, 2, "", 0, 0, "", 1)
813 params[:pdf].RDMCell(2, 2, "", 0, 0, "", 1)
805 end
814 end
806 if coords[:end]
815 if coords[:end]
807 params[:pdf].SetY(params[:top] + 1)
816 params[:pdf].SetY(params[:top] + 1)
808 params[:pdf].SetX(params[:subject_width] + coords[:end] - 1)
817 params[:pdf].SetX(params[:subject_width] + coords[:end] - 1)
809 params[:pdf].SetFillColor(50,50,200)
818 params[:pdf].SetFillColor(50,50,200)
810 params[:pdf].RDMCell(2, 2, "", 0, 0, "", 1)
819 params[:pdf].RDMCell(2, 2, "", 0, 0, "", 1)
811 end
820 end
812 end
821 end
813 # Renders the label on the right
822 # Renders the label on the right
814 if options[:label]
823 if options[:label]
815 params[:pdf].SetX(params[:subject_width] + (coords[:bar_end] || 0) + 5)
824 params[:pdf].SetX(params[:subject_width] + (coords[:bar_end] || 0) + 5)
816 params[:pdf].RDMCell(30, 2, options[:label])
825 params[:pdf].RDMCell(30, 2, options[:label])
817 end
826 end
818 end
827 end
819
828
820 def image_task(params, coords, options={})
829 def image_task(params, coords, options={})
821 height = options[:height] || 6
830 height = options[:height] || 6
822
831
823 # Renders the task bar, with progress and late
832 # Renders the task bar, with progress and late
824 if coords[:bar_start] && coords[:bar_end]
833 if coords[:bar_start] && coords[:bar_end]
825 params[:image].fill('#aaa')
834 params[:image].fill('#aaa')
826 params[:image].rectangle(params[:subject_width] + coords[:bar_start], params[:top], params[:subject_width] + coords[:bar_end], params[:top] - height)
835 params[:image].rectangle(params[:subject_width] + coords[:bar_start], params[:top], params[:subject_width] + coords[:bar_end], params[:top] - height)
827
836
828 if coords[:bar_late_end]
837 if coords[:bar_late_end]
829 params[:image].fill('#f66')
838 params[:image].fill('#f66')
830 params[:image].rectangle(params[:subject_width] + coords[:bar_start], params[:top], params[:subject_width] + coords[:bar_late_end], params[:top] - height)
839 params[:image].rectangle(params[:subject_width] + coords[:bar_start], params[:top], params[:subject_width] + coords[:bar_late_end], params[:top] - height)
831 end
840 end
832 if coords[:bar_progress_end]
841 if coords[:bar_progress_end]
833 params[:image].fill('#00c600')
842 params[:image].fill('#00c600')
834 params[:image].rectangle(params[:subject_width] + coords[:bar_start], params[:top], params[:subject_width] + coords[:bar_progress_end], params[:top] - height)
843 params[:image].rectangle(params[:subject_width] + coords[:bar_start], params[:top], params[:subject_width] + coords[:bar_progress_end], params[:top] - height)
835 end
844 end
836 end
845 end
837 # Renders the markers
846 # Renders the markers
838 if options[:markers]
847 if options[:markers]
839 if coords[:start]
848 if coords[:start]
840 x = params[:subject_width] + coords[:start]
849 x = params[:subject_width] + coords[:start]
841 y = params[:top] - height / 2
850 y = params[:top] - height / 2
842 params[:image].fill('blue')
851 params[:image].fill('blue')
843 params[:image].polygon(x-4, y, x, y-4, x+4, y, x, y+4)
852 params[:image].polygon(x-4, y, x, y-4, x+4, y, x, y+4)
844 end
853 end
845 if coords[:end]
854 if coords[:end]
846 x = params[:subject_width] + coords[:end] + params[:zoom]
855 x = params[:subject_width] + coords[:end] + params[:zoom]
847 y = params[:top] - height / 2
856 y = params[:top] - height / 2
848 params[:image].fill('blue')
857 params[:image].fill('blue')
849 params[:image].polygon(x-4, y, x, y-4, x+4, y, x, y+4)
858 params[:image].polygon(x-4, y, x, y-4, x+4, y, x, y+4)
850 end
859 end
851 end
860 end
852 # Renders the label on the right
861 # Renders the label on the right
853 if options[:label]
862 if options[:label]
854 params[:image].fill('black')
863 params[:image].fill('black')
855 params[:image].text(params[:subject_width] + (coords[:bar_end] || 0) + 5,params[:top] + 1, options[:label])
864 params[:image].text(params[:subject_width] + (coords[:bar_end] || 0) + 5,params[:top] + 1, options[:label])
856 end
865 end
857 end
866 end
858 end
867 end
859 end
868 end
860 end
869 end
General Comments 0
You need to be logged in to leave comments. Login now