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