##// END OF EJS Templates
PDF: prepare switching TCPDF UTF-8 in non CJK or FPDF ANSI in CJK (#61)....
Toshi MARUYAMA -
r5138:988841d69a0d
parent child
Show More
@@ -1,319 +1,381
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
33 include Redmine::I18n
34 attr_accessor :footer_date
35
36 def initialize(lang)
37 super()
38 set_language_if_valid lang
39 @font_for_content = 'FreeSans'
40 @font_for_footer = 'FreeSans'
41 SetCreator(Redmine::Info.app_name)
42 SetFont(@font_for_content)
43 end
44
45 def SetFontStyle(style, size)
46 SetFont(@font_for_content, style, size)
47 end
48
49 def SetTitle(txt)
50 txt = begin
51 utf16txt = Iconv.conv('UTF-16BE', 'UTF-8', txt)
52 hextxt = "<FEFF" # FEFF is BOM
53 hextxt << utf16txt.unpack("C*").map {|x| sprintf("%02X",x) }.join
54 hextxt << ">"
55 rescue
56 txt
57 end || ''
58 super(txt)
59 end
60
61 def textstring(s)
62 # Format a text string
63 if s =~ /^</ # This means the string is hex-dumped.
64 return s
65 else
66 return '('+escape(s)+')'
67 end
68 end
69
70 alias RDMCell Cell
71 alias RDMMultiCell MultiCell
72
73 def Footer
74 SetFont(@font_for_footer, 'I', 8)
75 SetY(-15)
76 SetX(15)
77 RDMCell(0, 5, @footer_date, 0, 0, 'L')
78 SetY(-15)
79 SetX(-30)
80 RDMCell(0, 5, PageNo().to_s + '/{nb}', 0, 0, 'C')
81 end
82 end
83
32 class IFPDF < FPDF
84 class IFPDF < FPDF
33 include Redmine::I18n
85 include Redmine::I18n
34 attr_accessor :footer_date
86 attr_accessor :footer_date
35
87
36 def initialize(lang)
88 def initialize(lang)
37 super()
89 super()
38 set_language_if_valid lang
90 set_language_if_valid lang
39 case current_language.to_s.downcase
91 case current_language.to_s.downcase
40 when 'ko'
92 when 'ko'
41 extend(PDF_Korean)
93 extend(PDF_Korean)
42 AddUHCFont()
94 AddUHCFont()
43 @font_for_content = 'UHC'
95 @font_for_content = 'UHC'
44 @font_for_footer = 'UHC'
96 @font_for_footer = 'UHC'
45 when 'ja'
97 when 'ja'
46 extend(PDF_Japanese)
98 extend(PDF_Japanese)
47 AddSJISFont()
99 AddSJISFont()
48 @font_for_content = 'SJIS'
100 @font_for_content = 'SJIS'
49 @font_for_footer = 'SJIS'
101 @font_for_footer = 'SJIS'
50 when 'zh'
102 when 'zh'
51 extend(PDF_Chinese)
103 extend(PDF_Chinese)
52 AddGBFont()
104 AddGBFont()
53 @font_for_content = 'GB'
105 @font_for_content = 'GB'
54 @font_for_footer = 'GB'
106 @font_for_footer = 'GB'
55 when 'zh-tw'
107 when 'zh-tw'
56 extend(PDF_Chinese)
108 extend(PDF_Chinese)
57 AddBig5Font()
109 AddBig5Font()
58 @font_for_content = 'Big5'
110 @font_for_content = 'Big5'
59 @font_for_footer = 'Big5'
111 @font_for_footer = 'Big5'
60 else
112 else
61 @font_for_content = 'Arial'
113 @font_for_content = 'Arial'
62 @font_for_footer = 'Helvetica'
114 @font_for_footer = 'Helvetica'
63 end
115 end
64 SetCreator(Redmine::Info.app_name)
116 SetCreator(Redmine::Info.app_name)
65 SetFont(@font_for_content)
117 SetFont(@font_for_content)
66 end
118 end
67
119
68 def SetFontStyle(style, size)
120 def SetFontStyle(style, size)
69 SetFont(@font_for_content, style, size)
121 SetFont(@font_for_content, style, size)
70 end
122 end
71
123
72 def SetTitle(txt)
124 def SetTitle(txt)
73 txt = begin
125 txt = begin
74 utf16txt = Iconv.conv('UTF-16BE', 'UTF-8', txt)
126 utf16txt = Iconv.conv('UTF-16BE', 'UTF-8', txt)
75 hextxt = "<FEFF" # FEFF is BOM
127 hextxt = "<FEFF" # FEFF is BOM
76 hextxt << utf16txt.unpack("C*").map {|x| sprintf("%02X",x) }.join
128 hextxt << utf16txt.unpack("C*").map {|x| sprintf("%02X",x) }.join
77 hextxt << ">"
129 hextxt << ">"
78 rescue
130 rescue
79 txt
131 txt
80 end || ''
132 end || ''
81 super(txt)
133 super(txt)
82 end
134 end
83
135
84 def textstring(s)
136 def textstring(s)
85 # Format a text string
137 # Format a text string
86 if s =~ /^</ # This means the string is hex-dumped.
138 if s =~ /^</ # This means the string is hex-dumped.
87 return s
139 return s
88 else
140 else
89 return '('+escape(s)+')'
141 return '('+escape(s)+')'
90 end
142 end
91 end
143 end
92
144
93 def Cell(w,h=0,txt='',border=0,ln=0,align='',fill=0,link='')
145 def fix_text_encoding(txt)
94 @ic ||= Iconv.new(l(:general_pdf_encoding), 'UTF-8')
146 @ic ||= Iconv.new(l(:general_pdf_encoding), 'UTF-8')
95 # these quotation marks are not correctly rendered in the pdf
147 # these quotation marks are not correctly rendered in the pdf
96 txt = txt.gsub(/[Ò€œÒ€�]/, '"') if txt
148 txt = txt.gsub(/[Ò€œÒ€�]/, '"') if txt
97 txt = begin
149 txt = begin
98 # 0x5c char handling
150 # 0x5c char handling
99 txtar = txt.split('\\')
151 txtar = txt.split('\\')
100 txtar << '' if txt[-1] == ?\\
152 txtar << '' if txt[-1] == ?\\
101 txtar.collect {|x| @ic.iconv(x)}.join('\\').gsub(/\\/, "\\\\\\\\")
153 txtar.collect {|x| @ic.iconv(x)}.join('\\').gsub(/\\/, "\\\\\\\\")
102 rescue
154 rescue
103 txt
155 txt
104 end || ''
156 end || ''
105 super w,h,txt,border,ln,align,fill,link
157 return txt
158 end
159
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)
162 end
163
164 def RDMMultiCell(w,h=0,txt='',border=0,align='',fill=0)
165 MultiCell(w,h,fix_text_encoding(txt),border,align,fill)
106 end
166 end
107
167
108 def Footer
168 def Footer
109 SetFont(@font_for_footer, 'I', 8)
169 SetFont(@font_for_footer, 'I', 8)
110 SetY(-15)
170 SetY(-15)
111 SetX(15)
171 SetX(15)
112 Cell(0, 5, @footer_date, 0, 0, 'L')
172 RDMCell(0, 5, @footer_date, 0, 0, 'L')
113 SetY(-15)
173 SetY(-15)
114 SetX(-30)
174 SetX(-30)
115 Cell(0, 5, PageNo().to_s + '/{nb}', 0, 0, 'C')
175 RDMCell(0, 5, PageNo().to_s + '/{nb}', 0, 0, 'C')
116 end
176 end
177 alias alias_nb_pages AliasNbPages
117 end
178 end
118
179
119 # Returns a PDF string of a list of issues
180 # Returns a PDF string of a list of issues
120 def issues_to_pdf(issues, project, query)
181 def issues_to_pdf(issues, project, query)
121 pdf = IFPDF.new(current_language)
182 pdf = IFPDF.new(current_language)
183
122 title = query.new_record? ? l(:label_issue_plural) : query.name
184 title = query.new_record? ? l(:label_issue_plural) : query.name
123 title = "#{project} - #{title}" if project
185 title = "#{project} - #{title}" if project
124 pdf.SetTitle(title)
186 pdf.SetTitle(title)
125 pdf.AliasNbPages
187 pdf.alias_nb_pages
126 pdf.footer_date = format_date(Date.today)
188 pdf.footer_date = format_date(Date.today)
127 pdf.AddPage("L")
189 pdf.AddPage("L")
128
190
129 row_height = 6
191 row_height = 6
130 col_width = []
192 col_width = []
131 unless query.columns.empty?
193 unless query.columns.empty?
132 col_width = query.columns.collect {|column| column.name == :subject ? 4.0 : 1.0 }
194 col_width = query.columns.collect {|column| column.name == :subject ? 4.0 : 1.0 }
133 ratio = 262.0 / col_width.inject(0) {|s,w| s += w}
195 ratio = 262.0 / col_width.inject(0) {|s,w| s += w}
134 col_width = col_width.collect {|w| w * ratio}
196 col_width = col_width.collect {|w| w * ratio}
135 end
197 end
136
198
137 # title
199 # title
138 pdf.SetFontStyle('B',11)
200 pdf.SetFontStyle('B',11)
139 pdf.Cell(190,10, title)
201 pdf.RDMCell(190,10, title)
140 pdf.Ln
202 pdf.Ln
141
203
142 # headers
204 # headers
143 pdf.SetFontStyle('B',8)
205 pdf.SetFontStyle('B',8)
144 pdf.SetFillColor(230, 230, 230)
206 pdf.SetFillColor(230, 230, 230)
145 pdf.Cell(15, row_height, "#", 1, 0, 'L', 1)
207 pdf.RDMCell(15, row_height, "#", 1, 0, 'L', 1)
146 query.columns.each_with_index do |column, i|
208 query.columns.each_with_index do |column, i|
147 pdf.Cell(col_width[i], row_height, column.caption, 1, 0, 'L', 1)
209 pdf.RDMCell(col_width[i], row_height, column.caption, 1, 0, 'L', 1)
148 end
210 end
149 pdf.Ln
211 pdf.Ln
150
212
151 # rows
213 # rows
152 pdf.SetFontStyle('',8)
214 pdf.SetFontStyle('',8)
153 pdf.SetFillColor(255, 255, 255)
215 pdf.SetFillColor(255, 255, 255)
154 previous_group = false
216 previous_group = false
155 issues.each do |issue|
217 issues.each do |issue|
156 if query.grouped? && (group = query.group_by_column.value(issue)) != previous_group
218 if query.grouped? && (group = query.group_by_column.value(issue)) != previous_group
157 pdf.SetFontStyle('B',9)
219 pdf.SetFontStyle('B',9)
158 pdf.Cell(277, row_height,
220 pdf.RDMCell(277, row_height,
159 (group.blank? ? 'None' : group.to_s) + " (#{query.issue_count_by_group[group]})",
221 (group.blank? ? 'None' : group.to_s) + " (#{query.issue_count_by_group[group]})",
160 1, 1, 'L')
222 1, 1, 'L')
161 pdf.SetFontStyle('',8)
223 pdf.SetFontStyle('',8)
162 previous_group = group
224 previous_group = group
163 end
225 end
164 pdf.Cell(15, row_height, issue.id.to_s, 1, 0, 'L', 1)
226 pdf.RDMCell(15, row_height, issue.id.to_s, 1, 0, 'L', 1)
165 query.columns.each_with_index do |column, i|
227 query.columns.each_with_index do |column, i|
166 s = if column.is_a?(QueryCustomFieldColumn)
228 s = if column.is_a?(QueryCustomFieldColumn)
167 cv = issue.custom_values.detect {|v| v.custom_field_id == column.custom_field.id}
229 cv = issue.custom_values.detect {|v| v.custom_field_id == column.custom_field.id}
168 show_value(cv)
230 show_value(cv)
169 else
231 else
170 value = issue.send(column.name)
232 value = issue.send(column.name)
171 if value.is_a?(Date)
233 if value.is_a?(Date)
172 format_date(value)
234 format_date(value)
173 elsif value.is_a?(Time)
235 elsif value.is_a?(Time)
174 format_time(value)
236 format_time(value)
175 else
237 else
176 value
238 value
177 end
239 end
178 end
240 end
179 pdf.Cell(col_width[i], row_height, s.to_s, 1, 0, 'L', 1)
241 pdf.RDMCell(col_width[i], row_height, s.to_s, 1, 0, 'L', 1)
180 end
242 end
181 pdf.Ln
243 pdf.Ln
182 end
244 end
183 if issues.size == Setting.issues_export_limit.to_i
245 if issues.size == Setting.issues_export_limit.to_i
184 pdf.SetFontStyle('B',10)
246 pdf.SetFontStyle('B',10)
185 pdf.Cell(0, row_height, '...')
247 pdf.RDMCell(0, row_height, '...')
186 end
248 end
187 pdf.Output
249 pdf.Output
188 end
250 end
189
251
190 # Returns a PDF string of a single issue
252 # Returns a PDF string of a single issue
191 def issue_to_pdf(issue)
253 def issue_to_pdf(issue)
192 pdf = IFPDF.new(current_language)
254 pdf = IFPDF.new(current_language)
193 pdf.SetTitle("#{issue.project} - ##{issue.tracker} #{issue.id}")
255 pdf.SetTitle("#{issue.project} - ##{issue.tracker} #{issue.id}")
194 pdf.AliasNbPages
256 pdf.alias_nb_pages
195 pdf.footer_date = format_date(Date.today)
257 pdf.footer_date = format_date(Date.today)
196 pdf.AddPage
258 pdf.AddPage
197
259
198 pdf.SetFontStyle('B',11)
260 pdf.SetFontStyle('B',11)
199 pdf.Cell(190,10, "#{issue.project} - #{issue.tracker} # #{issue.id}: #{issue.subject}")
261 pdf.RDMCell(190,10, "#{issue.project} - #{issue.tracker} # #{issue.id}: #{issue.subject}")
200 pdf.Ln
262 pdf.Ln
201
263
202 y0 = pdf.GetY
264 y0 = pdf.GetY
203
265
204 pdf.SetFontStyle('B',9)
266 pdf.SetFontStyle('B',9)
205 pdf.Cell(35,5, l(:field_status) + ":","LT")
267 pdf.RDMCell(35,5, l(:field_status) + ":","LT")
206 pdf.SetFontStyle('',9)
268 pdf.SetFontStyle('',9)
207 pdf.Cell(60,5, issue.status.to_s,"RT")
269 pdf.RDMCell(60,5, issue.status.to_s,"RT")
208 pdf.SetFontStyle('B',9)
270 pdf.SetFontStyle('B',9)
209 pdf.Cell(35,5, l(:field_priority) + ":","LT")
271 pdf.RDMCell(35,5, l(:field_priority) + ":","LT")
210 pdf.SetFontStyle('',9)
272 pdf.SetFontStyle('',9)
211 pdf.Cell(60,5, issue.priority.to_s,"RT")
273 pdf.RDMCell(60,5, issue.priority.to_s,"RT")
212 pdf.Ln
274 pdf.Ln
213
275
214 pdf.SetFontStyle('B',9)
276 pdf.SetFontStyle('B',9)
215 pdf.Cell(35,5, l(:field_author) + ":","L")
277 pdf.RDMCell(35,5, l(:field_author) + ":","L")
216 pdf.SetFontStyle('',9)
278 pdf.SetFontStyle('',9)
217 pdf.Cell(60,5, issue.author.to_s,"R")
279 pdf.RDMCell(60,5, issue.author.to_s,"R")
218 pdf.SetFontStyle('B',9)
280 pdf.SetFontStyle('B',9)
219 pdf.Cell(35,5, l(:field_category) + ":","L")
281 pdf.RDMCell(35,5, l(:field_category) + ":","L")
220 pdf.SetFontStyle('',9)
282 pdf.SetFontStyle('',9)
221 pdf.Cell(60,5, issue.category.to_s,"R")
283 pdf.RDMCell(60,5, issue.category.to_s,"R")
222 pdf.Ln
284 pdf.Ln
223
285
224 pdf.SetFontStyle('B',9)
286 pdf.SetFontStyle('B',9)
225 pdf.Cell(35,5, l(:field_created_on) + ":","L")
287 pdf.RDMCell(35,5, l(:field_created_on) + ":","L")
226 pdf.SetFontStyle('',9)
288 pdf.SetFontStyle('',9)
227 pdf.Cell(60,5, format_date(issue.created_on),"R")
289 pdf.RDMCell(60,5, format_date(issue.created_on),"R")
228 pdf.SetFontStyle('B',9)
290 pdf.SetFontStyle('B',9)
229 pdf.Cell(35,5, l(:field_assigned_to) + ":","L")
291 pdf.RDMCell(35,5, l(:field_assigned_to) + ":","L")
230 pdf.SetFontStyle('',9)
292 pdf.SetFontStyle('',9)
231 pdf.Cell(60,5, issue.assigned_to.to_s,"R")
293 pdf.RDMCell(60,5, issue.assigned_to.to_s,"R")
232 pdf.Ln
294 pdf.Ln
233
295
234 pdf.SetFontStyle('B',9)
296 pdf.SetFontStyle('B',9)
235 pdf.Cell(35,5, l(:field_updated_on) + ":","LB")
297 pdf.RDMCell(35,5, l(:field_updated_on) + ":","LB")
236 pdf.SetFontStyle('',9)
298 pdf.SetFontStyle('',9)
237 pdf.Cell(60,5, format_date(issue.updated_on),"RB")
299 pdf.RDMCell(60,5, format_date(issue.updated_on),"RB")
238 pdf.SetFontStyle('B',9)
300 pdf.SetFontStyle('B',9)
239 pdf.Cell(35,5, l(:field_due_date) + ":","LB")
301 pdf.RDMCell(35,5, l(:field_due_date) + ":","LB")
240 pdf.SetFontStyle('',9)
302 pdf.SetFontStyle('',9)
241 pdf.Cell(60,5, format_date(issue.due_date),"RB")
303 pdf.RDMCell(60,5, format_date(issue.due_date),"RB")
242 pdf.Ln
304 pdf.Ln
243
305
244 for custom_value in issue.custom_field_values
306 for custom_value in issue.custom_field_values
245 pdf.SetFontStyle('B',9)
307 pdf.SetFontStyle('B',9)
246 pdf.Cell(35,5, custom_value.custom_field.name + ":","L")
308 pdf.RDMCell(35,5, custom_value.custom_field.name + ":","L")
247 pdf.SetFontStyle('',9)
309 pdf.SetFontStyle('',9)
248 pdf.MultiCell(155,5, (show_value custom_value),"R")
310 pdf.RDMMultiCell(155,5, (show_value custom_value),"R")
249 end
311 end
250
312
251 pdf.SetFontStyle('B',9)
313 pdf.SetFontStyle('B',9)
252 pdf.Cell(35,5, l(:field_subject) + ":","LTB")
314 pdf.RDMCell(35,5, l(:field_subject) + ":","LTB")
253 pdf.SetFontStyle('',9)
315 pdf.SetFontStyle('',9)
254 pdf.Cell(155,5, issue.subject,"RTB")
316 pdf.RDMCell(155,5, issue.subject,"RTB")
255 pdf.Ln
317 pdf.Ln
256
318
257 pdf.SetFontStyle('B',9)
319 pdf.SetFontStyle('B',9)
258 pdf.Cell(35,5, l(:field_description) + ":")
320 pdf.RDMCell(35,5, l(:field_description) + ":")
259 pdf.SetFontStyle('',9)
321 pdf.SetFontStyle('',9)
260 pdf.MultiCell(155,5, issue.description.to_s,"BR")
322 pdf.RDMMultiCell(155,5, issue.description.to_s,"BR")
261
323
262 pdf.Line(pdf.GetX, y0, pdf.GetX, pdf.GetY)
324 pdf.Line(pdf.GetX, y0, pdf.GetX, pdf.GetY)
263 pdf.Line(pdf.GetX, pdf.GetY, 170, pdf.GetY)
325 pdf.Line(pdf.GetX, pdf.GetY, 170, pdf.GetY)
264 pdf.Ln
326 pdf.Ln
265
327
266 if issue.changesets.any? && User.current.allowed_to?(:view_changesets, issue.project)
328 if issue.changesets.any? && User.current.allowed_to?(:view_changesets, issue.project)
267 pdf.SetFontStyle('B',9)
329 pdf.SetFontStyle('B',9)
268 pdf.Cell(190,5, l(:label_associated_revisions), "B")
330 pdf.RDMCell(190,5, l(:label_associated_revisions), "B")
269 pdf.Ln
331 pdf.Ln
270 for changeset in issue.changesets
332 for changeset in issue.changesets
271 pdf.SetFontStyle('B',8)
333 pdf.SetFontStyle('B',8)
272 pdf.Cell(190,5, format_time(changeset.committed_on) + " - " + changeset.author.to_s)
334 pdf.RDMCell(190,5, format_time(changeset.committed_on) + " - " + changeset.author.to_s)
273 pdf.Ln
335 pdf.Ln
274 unless changeset.comments.blank?
336 unless changeset.comments.blank?
275 pdf.SetFontStyle('',8)
337 pdf.SetFontStyle('',8)
276 pdf.MultiCell(190,5, changeset.comments.to_s)
338 pdf.RDMMultiCell(190,5, changeset.comments.to_s)
277 end
339 end
278 pdf.Ln
340 pdf.Ln
279 end
341 end
280 end
342 end
281
343
282 pdf.SetFontStyle('B',9)
344 pdf.SetFontStyle('B',9)
283 pdf.Cell(190,5, l(:label_history), "B")
345 pdf.RDMCell(190,5, l(:label_history), "B")
284 pdf.Ln
346 pdf.Ln
285 for journal in issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
347 for journal in issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
286 pdf.SetFontStyle('B',8)
348 pdf.SetFontStyle('B',8)
287 pdf.Cell(190,5, format_time(journal.created_on) + " - " + journal.user.name)
349 pdf.RDMCell(190,5, format_time(journal.created_on) + " - " + journal.user.name)
288 pdf.Ln
350 pdf.Ln
289 pdf.SetFontStyle('I',8)
351 pdf.SetFontStyle('I',8)
290 for detail in journal.details
352 for detail in journal.details
291 pdf.Cell(190,5, "- " + show_detail(detail, true))
353 pdf.RDMCell(190,5, "- " + show_detail(detail, true))
292 pdf.Ln
354 pdf.Ln
293 end
355 end
294 if journal.notes?
356 if journal.notes?
295 pdf.SetFontStyle('',8)
357 pdf.SetFontStyle('',8)
296 pdf.MultiCell(190,5, journal.notes.to_s)
358 pdf.RDMMultiCell(190,5, journal.notes.to_s)
297 end
359 end
298 pdf.Ln
360 pdf.Ln
299 end
361 end
300
362
301 if issue.attachments.any?
363 if issue.attachments.any?
302 pdf.SetFontStyle('B',9)
364 pdf.SetFontStyle('B',9)
303 pdf.Cell(190,5, l(:label_attachment_plural), "B")
365 pdf.RDMCell(190,5, l(:label_attachment_plural), "B")
304 pdf.Ln
366 pdf.Ln
305 for attachment in issue.attachments
367 for attachment in issue.attachments
306 pdf.SetFontStyle('',8)
368 pdf.SetFontStyle('',8)
307 pdf.Cell(80,5, attachment.filename)
369 pdf.RDMCell(80,5, attachment.filename)
308 pdf.Cell(20,5, number_to_human_size(attachment.filesize),0,0,"R")
370 pdf.RDMCell(20,5, number_to_human_size(attachment.filesize),0,0,"R")
309 pdf.Cell(25,5, format_date(attachment.created_on),0,0,"R")
371 pdf.RDMCell(25,5, format_date(attachment.created_on),0,0,"R")
310 pdf.Cell(65,5, attachment.author.name,0,0,"R")
372 pdf.RDMCell(65,5, attachment.author.name,0,0,"R")
311 pdf.Ln
373 pdf.Ln
312 end
374 end
313 end
375 end
314 pdf.Output
376 pdf.Output
315 end
377 end
316
378
317 end
379 end
318 end
380 end
319 end
381 end
@@ -1,860 +1,860
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 pdf = ::Redmine::Export::PDF::IFPDF.new(current_language)
510 pdf = ::Redmine::Export::PDF::IFPDF.new(current_language)
511 pdf.SetTitle("#{l(:label_gantt)} #{project}")
511 pdf.SetTitle("#{l(:label_gantt)} #{project}")
512 pdf.AliasNbPages
512 pdf.alias_nb_pages
513 pdf.footer_date = format_date(Date.today)
513 pdf.footer_date = format_date(Date.today)
514 pdf.AddPage("L")
514 pdf.AddPage("L")
515 pdf.SetFontStyle('B',12)
515 pdf.SetFontStyle('B',12)
516 pdf.SetX(15)
516 pdf.SetX(15)
517 pdf.Cell(PDF::LeftPaneWidth, 20, project.to_s)
517 pdf.RDMCell(PDF::LeftPaneWidth, 20, project.to_s)
518 pdf.Ln
518 pdf.Ln
519 pdf.SetFontStyle('B',9)
519 pdf.SetFontStyle('B',9)
520
520
521 subject_width = PDF::LeftPaneWidth
521 subject_width = PDF::LeftPaneWidth
522 header_heigth = 5
522 header_heigth = 5
523
523
524 headers_heigth = header_heigth
524 headers_heigth = header_heigth
525 show_weeks = false
525 show_weeks = false
526 show_days = false
526 show_days = false
527
527
528 if self.months < 7
528 if self.months < 7
529 show_weeks = true
529 show_weeks = true
530 headers_heigth = 2*header_heigth
530 headers_heigth = 2*header_heigth
531 if self.months < 3
531 if self.months < 3
532 show_days = true
532 show_days = true
533 headers_heigth = 3*header_heigth
533 headers_heigth = 3*header_heigth
534 end
534 end
535 end
535 end
536
536
537 g_width = PDF.right_pane_width
537 g_width = PDF.right_pane_width
538 zoom = (g_width) / (self.date_to - self.date_from + 1)
538 zoom = (g_width) / (self.date_to - self.date_from + 1)
539 g_height = 120
539 g_height = 120
540 t_height = g_height + headers_heigth
540 t_height = g_height + headers_heigth
541
541
542 y_start = pdf.GetY
542 y_start = pdf.GetY
543
543
544 # Months headers
544 # Months headers
545 month_f = self.date_from
545 month_f = self.date_from
546 left = subject_width
546 left = subject_width
547 height = header_heigth
547 height = header_heigth
548 self.months.times do
548 self.months.times do
549 width = ((month_f >> 1) - month_f) * zoom
549 width = ((month_f >> 1) - month_f) * zoom
550 pdf.SetY(y_start)
550 pdf.SetY(y_start)
551 pdf.SetX(left)
551 pdf.SetX(left)
552 pdf.Cell(width, height, "#{month_f.year}-#{month_f.month}", "LTR", 0, "C")
552 pdf.RDMCell(width, height, "#{month_f.year}-#{month_f.month}", "LTR", 0, "C")
553 left = left + width
553 left = left + width
554 month_f = month_f >> 1
554 month_f = month_f >> 1
555 end
555 end
556
556
557 # Weeks headers
557 # Weeks headers
558 if show_weeks
558 if show_weeks
559 left = subject_width
559 left = subject_width
560 height = header_heigth
560 height = header_heigth
561 if self.date_from.cwday == 1
561 if self.date_from.cwday == 1
562 # self.date_from is monday
562 # self.date_from is monday
563 week_f = self.date_from
563 week_f = self.date_from
564 else
564 else
565 # find next monday after self.date_from
565 # find next monday after self.date_from
566 week_f = self.date_from + (7 - self.date_from.cwday + 1)
566 week_f = self.date_from + (7 - self.date_from.cwday + 1)
567 width = (7 - self.date_from.cwday + 1) * zoom-1
567 width = (7 - self.date_from.cwday + 1) * zoom-1
568 pdf.SetY(y_start + header_heigth)
568 pdf.SetY(y_start + header_heigth)
569 pdf.SetX(left)
569 pdf.SetX(left)
570 pdf.Cell(width + 1, height, "", "LTR")
570 pdf.RDMCell(width + 1, height, "", "LTR")
571 left = left + width+1
571 left = left + width+1
572 end
572 end
573 while week_f <= self.date_to
573 while week_f <= self.date_to
574 width = (week_f + 6 <= self.date_to) ? 7 * zoom : (self.date_to - week_f + 1) * zoom
574 width = (week_f + 6 <= self.date_to) ? 7 * zoom : (self.date_to - week_f + 1) * zoom
575 pdf.SetY(y_start + header_heigth)
575 pdf.SetY(y_start + header_heigth)
576 pdf.SetX(left)
576 pdf.SetX(left)
577 pdf.Cell(width, height, (width >= 5 ? week_f.cweek.to_s : ""), "LTR", 0, "C")
577 pdf.RDMCell(width, height, (width >= 5 ? week_f.cweek.to_s : ""), "LTR", 0, "C")
578 left = left + width
578 left = left + width
579 week_f = week_f+7
579 week_f = week_f+7
580 end
580 end
581 end
581 end
582
582
583 # Days headers
583 # Days headers
584 if show_days
584 if show_days
585 left = subject_width
585 left = subject_width
586 height = header_heigth
586 height = header_heigth
587 wday = self.date_from.cwday
587 wday = self.date_from.cwday
588 pdf.SetFontStyle('B',7)
588 pdf.SetFontStyle('B',7)
589 (self.date_to - self.date_from + 1).to_i.times do
589 (self.date_to - self.date_from + 1).to_i.times do
590 width = zoom
590 width = zoom
591 pdf.SetY(y_start + 2 * header_heigth)
591 pdf.SetY(y_start + 2 * header_heigth)
592 pdf.SetX(left)
592 pdf.SetX(left)
593 pdf.Cell(width, height, day_name(wday).first, "LTR", 0, "C")
593 pdf.RDMCell(width, height, day_name(wday).first, "LTR", 0, "C")
594 left = left + width
594 left = left + width
595 wday = wday + 1
595 wday = wday + 1
596 wday = 1 if wday > 7
596 wday = 1 if wday > 7
597 end
597 end
598 end
598 end
599
599
600 pdf.SetY(y_start)
600 pdf.SetY(y_start)
601 pdf.SetX(15)
601 pdf.SetX(15)
602 pdf.Cell(subject_width+g_width-15, headers_heigth, "", 1)
602 pdf.RDMCell(subject_width+g_width-15, headers_heigth, "", 1)
603
603
604 # Tasks
604 # Tasks
605 top = headers_heigth + y_start
605 top = headers_heigth + y_start
606 options = {
606 options = {
607 :top => top,
607 :top => top,
608 :zoom => zoom,
608 :zoom => zoom,
609 :subject_width => subject_width,
609 :subject_width => subject_width,
610 :g_width => g_width,
610 :g_width => g_width,
611 :indent => 0,
611 :indent => 0,
612 :indent_increment => 5,
612 :indent_increment => 5,
613 :top_increment => 5,
613 :top_increment => 5,
614 :format => :pdf,
614 :format => :pdf,
615 :pdf => pdf
615 :pdf => pdf
616 }
616 }
617 render(options)
617 render(options)
618 pdf.Output
618 pdf.Output
619 end
619 end
620
620
621 private
621 private
622
622
623 def coordinates(start_date, end_date, progress, zoom=nil)
623 def coordinates(start_date, end_date, progress, zoom=nil)
624 zoom ||= @zoom
624 zoom ||= @zoom
625
625
626 coords = {}
626 coords = {}
627 if start_date && end_date && start_date < self.date_to && end_date > self.date_from
627 if start_date && end_date && start_date < self.date_to && end_date > self.date_from
628 if start_date > self.date_from
628 if start_date > self.date_from
629 coords[:start] = start_date - self.date_from
629 coords[:start] = start_date - self.date_from
630 coords[:bar_start] = start_date - self.date_from
630 coords[:bar_start] = start_date - self.date_from
631 else
631 else
632 coords[:bar_start] = 0
632 coords[:bar_start] = 0
633 end
633 end
634 if end_date < self.date_to
634 if end_date < self.date_to
635 coords[:end] = end_date - self.date_from
635 coords[:end] = end_date - self.date_from
636 coords[:bar_end] = end_date - self.date_from + 1
636 coords[:bar_end] = end_date - self.date_from + 1
637 else
637 else
638 coords[:bar_end] = self.date_to - self.date_from + 1
638 coords[:bar_end] = self.date_to - self.date_from + 1
639 end
639 end
640
640
641 if progress
641 if progress
642 progress_date = start_date + (end_date - start_date + 1) * (progress / 100.0)
642 progress_date = start_date + (end_date - start_date + 1) * (progress / 100.0)
643 if progress_date > self.date_from && progress_date > start_date
643 if progress_date > self.date_from && progress_date > start_date
644 if progress_date < self.date_to
644 if progress_date < self.date_to
645 coords[:bar_progress_end] = progress_date - self.date_from
645 coords[:bar_progress_end] = progress_date - self.date_from
646 else
646 else
647 coords[:bar_progress_end] = self.date_to - self.date_from + 1
647 coords[:bar_progress_end] = self.date_to - self.date_from + 1
648 end
648 end
649 end
649 end
650
650
651 if progress_date < Date.today
651 if progress_date < Date.today
652 late_date = [Date.today, end_date].min
652 late_date = [Date.today, end_date].min
653 if late_date > self.date_from && late_date > start_date
653 if late_date > self.date_from && late_date > start_date
654 if late_date < self.date_to
654 if late_date < self.date_to
655 coords[:bar_late_end] = late_date - self.date_from + 1
655 coords[:bar_late_end] = late_date - self.date_from + 1
656 else
656 else
657 coords[:bar_late_end] = self.date_to - self.date_from + 1
657 coords[:bar_late_end] = self.date_to - self.date_from + 1
658 end
658 end
659 end
659 end
660 end
660 end
661 end
661 end
662 end
662 end
663
663
664 # Transforms dates into pixels witdh
664 # Transforms dates into pixels witdh
665 coords.keys.each do |key|
665 coords.keys.each do |key|
666 coords[key] = (coords[key] * zoom).floor
666 coords[key] = (coords[key] * zoom).floor
667 end
667 end
668 coords
668 coords
669 end
669 end
670
670
671 # Sorts a collection of issues by start_date, due_date, id for gantt rendering
671 # Sorts a collection of issues by start_date, due_date, id for gantt rendering
672 def sort_issues!(issues)
672 def sort_issues!(issues)
673 issues.sort! { |a, b| gantt_issue_compare(a, b, issues) }
673 issues.sort! { |a, b| gantt_issue_compare(a, b, issues) }
674 end
674 end
675
675
676 # TODO: top level issues should be sorted by start date
676 # TODO: top level issues should be sorted by start date
677 def gantt_issue_compare(x, y, issues)
677 def gantt_issue_compare(x, y, issues)
678 if x.root_id == y.root_id
678 if x.root_id == y.root_id
679 x.lft <=> y.lft
679 x.lft <=> y.lft
680 else
680 else
681 x.root_id <=> y.root_id
681 x.root_id <=> y.root_id
682 end
682 end
683 end
683 end
684
684
685 def current_limit
685 def current_limit
686 if @max_rows
686 if @max_rows
687 @max_rows - @number_of_rows
687 @max_rows - @number_of_rows
688 else
688 else
689 nil
689 nil
690 end
690 end
691 end
691 end
692
692
693 def abort?
693 def abort?
694 if @max_rows && @number_of_rows >= @max_rows
694 if @max_rows && @number_of_rows >= @max_rows
695 @truncated = true
695 @truncated = true
696 end
696 end
697 end
697 end
698
698
699 def pdf_new_page?(options)
699 def pdf_new_page?(options)
700 if options[:top] > 180
700 if options[:top] > 180
701 options[:pdf].Line(15, options[:top], PDF::TotalWidth, options[:top])
701 options[:pdf].Line(15, options[:top], PDF::TotalWidth, options[:top])
702 options[:pdf].AddPage("L")
702 options[:pdf].AddPage("L")
703 options[:top] = 15
703 options[:top] = 15
704 options[:pdf].Line(15, options[:top] - 0.1, PDF::TotalWidth, options[:top] - 0.1)
704 options[:pdf].Line(15, options[:top] - 0.1, PDF::TotalWidth, options[:top] - 0.1)
705 end
705 end
706 end
706 end
707
707
708 def html_subject(params, subject, options={})
708 def html_subject(params, subject, options={})
709 style = "position: absolute;top:#{params[:top]}px;left:#{params[:indent]}px;"
709 style = "position: absolute;top:#{params[:top]}px;left:#{params[:indent]}px;"
710 style << "width:#{params[:subject_width] - params[:indent]}px;" if params[:subject_width]
710 style << "width:#{params[:subject_width] - params[:indent]}px;" if params[:subject_width]
711
711
712 output = view.content_tag 'div', subject, :class => options[:css], :style => style, :title => options[:title]
712 output = view.content_tag 'div', subject, :class => options[:css], :style => style, :title => options[:title]
713 @subjects << output
713 @subjects << output
714 output
714 output
715 end
715 end
716
716
717 def pdf_subject(params, subject, options={})
717 def pdf_subject(params, subject, options={})
718 params[:pdf].SetY(params[:top])
718 params[:pdf].SetY(params[:top])
719 params[:pdf].SetX(15)
719 params[:pdf].SetX(15)
720
720
721 char_limit = PDF::MaxCharactorsForSubject - params[:indent]
721 char_limit = PDF::MaxCharactorsForSubject - params[:indent]
722 params[:pdf].Cell(params[:subject_width]-15, 5, (" " * params[:indent]) + subject.to_s.sub(/^(.{#{char_limit}}[^\s]*\s).*$/, '\1 (...)'), "LR")
722 params[:pdf].RDMCell(params[:subject_width]-15, 5, (" " * params[:indent]) + subject.to_s.sub(/^(.{#{char_limit}}[^\s]*\s).*$/, '\1 (...)'), "LR")
723
723
724 params[:pdf].SetY(params[:top])
724 params[:pdf].SetY(params[:top])
725 params[:pdf].SetX(params[:subject_width])
725 params[:pdf].SetX(params[:subject_width])
726 params[:pdf].Cell(params[:g_width], 5, "", "LR")
726 params[:pdf].RDMCell(params[:g_width], 5, "", "LR")
727 end
727 end
728
728
729 def image_subject(params, subject, options={})
729 def image_subject(params, subject, options={})
730 params[:image].fill('black')
730 params[:image].fill('black')
731 params[:image].stroke('transparent')
731 params[:image].stroke('transparent')
732 params[:image].stroke_width(1)
732 params[:image].stroke_width(1)
733 params[:image].text(params[:indent], params[:top] + 2, subject)
733 params[:image].text(params[:indent], params[:top] + 2, subject)
734 end
734 end
735
735
736 def html_task(params, coords, options={})
736 def html_task(params, coords, options={})
737 output = ''
737 output = ''
738 # Renders the task bar, with progress and late
738 # Renders the task bar, with progress and late
739 if coords[:bar_start] && coords[:bar_end]
739 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>"
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>"
741
741
742 if coords[:bar_late_end]
742 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>"
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>"
744 end
744 end
745 if coords[:bar_progress_end]
745 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>"
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>"
747 end
747 end
748 end
748 end
749 # Renders the markers
749 # Renders the markers
750 if options[:markers]
750 if options[:markers]
751 if coords[:start]
751 if coords[:start]
752 output << "<div style='top:#{ params[:top] }px;left:#{ coords[:start] }px;width:15px;' class='#{options[:css]} marker starting'>&nbsp;</div>"
752 output << "<div style='top:#{ params[:top] }px;left:#{ coords[:start] }px;width:15px;' class='#{options[:css]} marker starting'>&nbsp;</div>"
753 end
753 end
754 if coords[:end]
754 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>"
755 output << "<div style='top:#{ params[:top] }px;left:#{ coords[:end] + params[:zoom] }px;width:15px;' class='#{options[:css]} marker ending'>&nbsp;</div>"
756 end
756 end
757 end
757 end
758 # Renders the label on the right
758 # Renders the label on the right
759 if options[:label]
759 if options[:label]
760 output << "<div style='top:#{ params[:top] }px;left:#{ (coords[:bar_end] || 0) + 8 }px;' class='#{options[:css]} label'>"
760 output << "<div style='top:#{ params[:top] }px;left:#{ (coords[:bar_end] || 0) + 8 }px;' class='#{options[:css]} label'>"
761 output << options[:label]
761 output << options[:label]
762 output << "</div>"
762 output << "</div>"
763 end
763 end
764 # Renders the tooltip
764 # Renders the tooltip
765 if options[:issue] && coords[:bar_start] && coords[:bar_end]
765 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;'>"
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;'>"
767 output << '<span class="tip">'
767 output << '<span class="tip">'
768 output << view.render_issue_tooltip(options[:issue])
768 output << view.render_issue_tooltip(options[:issue])
769 output << "</span></div>"
769 output << "</span></div>"
770 end
770 end
771 @lines << output
771 @lines << output
772 output
772 output
773 end
773 end
774
774
775 def pdf_task(params, coords, options={})
775 def pdf_task(params, coords, options={})
776 height = options[:height] || 2
776 height = options[:height] || 2
777
777
778 # Renders the task bar, with progress and late
778 # Renders the task bar, with progress and late
779 if coords[:bar_start] && coords[:bar_end]
779 if coords[:bar_start] && coords[:bar_end]
780 params[:pdf].SetY(params[:top]+1.5)
780 params[:pdf].SetY(params[:top]+1.5)
781 params[:pdf].SetX(params[:subject_width] + coords[:bar_start])
781 params[:pdf].SetX(params[:subject_width] + coords[:bar_start])
782 params[:pdf].SetFillColor(200,200,200)
782 params[:pdf].SetFillColor(200,200,200)
783 params[:pdf].Cell(coords[:bar_end] - coords[:bar_start], height, "", 0, 0, "", 1)
783 params[:pdf].RDMCell(coords[:bar_end] - coords[:bar_start], height, "", 0, 0, "", 1)
784
784
785 if coords[:bar_late_end]
785 if coords[:bar_late_end]
786 params[:pdf].SetY(params[:top]+1.5)
786 params[:pdf].SetY(params[:top]+1.5)
787 params[:pdf].SetX(params[:subject_width] + coords[:bar_start])
787 params[:pdf].SetX(params[:subject_width] + coords[:bar_start])
788 params[:pdf].SetFillColor(255,100,100)
788 params[:pdf].SetFillColor(255,100,100)
789 params[:pdf].Cell(coords[:bar_late_end] - coords[:bar_start], height, "", 0, 0, "", 1)
789 params[:pdf].RDMCell(coords[:bar_late_end] - coords[:bar_start], height, "", 0, 0, "", 1)
790 end
790 end
791 if coords[:bar_progress_end]
791 if coords[:bar_progress_end]
792 params[:pdf].SetY(params[:top]+1.5)
792 params[:pdf].SetY(params[:top]+1.5)
793 params[:pdf].SetX(params[:subject_width] + coords[:bar_start])
793 params[:pdf].SetX(params[:subject_width] + coords[:bar_start])
794 params[:pdf].SetFillColor(90,200,90)
794 params[:pdf].SetFillColor(90,200,90)
795 params[:pdf].Cell(coords[:bar_progress_end] - coords[:bar_start], height, "", 0, 0, "", 1)
795 params[:pdf].RDMCell(coords[:bar_progress_end] - coords[:bar_start], height, "", 0, 0, "", 1)
796 end
796 end
797 end
797 end
798 # Renders the markers
798 # Renders the markers
799 if options[:markers]
799 if options[:markers]
800 if coords[:start]
800 if coords[:start]
801 params[:pdf].SetY(params[:top] + 1)
801 params[:pdf].SetY(params[:top] + 1)
802 params[:pdf].SetX(params[:subject_width] + coords[:start] - 1)
802 params[:pdf].SetX(params[:subject_width] + coords[:start] - 1)
803 params[:pdf].SetFillColor(50,50,200)
803 params[:pdf].SetFillColor(50,50,200)
804 params[:pdf].Cell(2, 2, "", 0, 0, "", 1)
804 params[:pdf].RDMCell(2, 2, "", 0, 0, "", 1)
805 end
805 end
806 if coords[:end]
806 if coords[:end]
807 params[:pdf].SetY(params[:top] + 1)
807 params[:pdf].SetY(params[:top] + 1)
808 params[:pdf].SetX(params[:subject_width] + coords[:end] - 1)
808 params[:pdf].SetX(params[:subject_width] + coords[:end] - 1)
809 params[:pdf].SetFillColor(50,50,200)
809 params[:pdf].SetFillColor(50,50,200)
810 params[:pdf].Cell(2, 2, "", 0, 0, "", 1)
810 params[:pdf].RDMCell(2, 2, "", 0, 0, "", 1)
811 end
811 end
812 end
812 end
813 # Renders the label on the right
813 # Renders the label on the right
814 if options[:label]
814 if options[:label]
815 params[:pdf].SetX(params[:subject_width] + (coords[:bar_end] || 0) + 5)
815 params[:pdf].SetX(params[:subject_width] + (coords[:bar_end] || 0) + 5)
816 params[:pdf].Cell(30, 2, options[:label])
816 params[:pdf].RDMCell(30, 2, options[:label])
817 end
817 end
818 end
818 end
819
819
820 def image_task(params, coords, options={})
820 def image_task(params, coords, options={})
821 height = options[:height] || 6
821 height = options[:height] || 6
822
822
823 # Renders the task bar, with progress and late
823 # Renders the task bar, with progress and late
824 if coords[:bar_start] && coords[:bar_end]
824 if coords[:bar_start] && coords[:bar_end]
825 params[:image].fill('#aaa')
825 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)
826 params[:image].rectangle(params[:subject_width] + coords[:bar_start], params[:top], params[:subject_width] + coords[:bar_end], params[:top] - height)
827
827
828 if coords[:bar_late_end]
828 if coords[:bar_late_end]
829 params[:image].fill('#f66')
829 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)
830 params[:image].rectangle(params[:subject_width] + coords[:bar_start], params[:top], params[:subject_width] + coords[:bar_late_end], params[:top] - height)
831 end
831 end
832 if coords[:bar_progress_end]
832 if coords[:bar_progress_end]
833 params[:image].fill('#00c600')
833 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)
834 params[:image].rectangle(params[:subject_width] + coords[:bar_start], params[:top], params[:subject_width] + coords[:bar_progress_end], params[:top] - height)
835 end
835 end
836 end
836 end
837 # Renders the markers
837 # Renders the markers
838 if options[:markers]
838 if options[:markers]
839 if coords[:start]
839 if coords[:start]
840 x = params[:subject_width] + coords[:start]
840 x = params[:subject_width] + coords[:start]
841 y = params[:top] - height / 2
841 y = params[:top] - height / 2
842 params[:image].fill('blue')
842 params[:image].fill('blue')
843 params[:image].polygon(x-4, y, x, y-4, x+4, y, x, y+4)
843 params[:image].polygon(x-4, y, x, y-4, x+4, y, x, y+4)
844 end
844 end
845 if coords[:end]
845 if coords[:end]
846 x = params[:subject_width] + coords[:end] + params[:zoom]
846 x = params[:subject_width] + coords[:end] + params[:zoom]
847 y = params[:top] - height / 2
847 y = params[:top] - height / 2
848 params[:image].fill('blue')
848 params[:image].fill('blue')
849 params[:image].polygon(x-4, y, x, y-4, x+4, y, x, y+4)
849 params[:image].polygon(x-4, y, x, y-4, x+4, y, x, y+4)
850 end
850 end
851 end
851 end
852 # Renders the label on the right
852 # Renders the label on the right
853 if options[:label]
853 if options[:label]
854 params[:image].fill('black')
854 params[:image].fill('black')
855 params[:image].text(params[:subject_width] + (coords[:bar_end] || 0) + 5,params[:top] + 1, options[:label])
855 params[:image].text(params[:subject_width] + (coords[:bar_end] || 0) + 5,params[:top] + 1, options[:label])
856 end
856 end
857 end
857 end
858 end
858 end
859 end
859 end
860 end
860 end
General Comments 0
You need to be logged in to leave comments. Login now