| 1 |
1 |
# Redmine - project management software
|
| 2 |
|
# Copyright (C) 2006-2008 Jean-Philippe Lang
|
|
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.
|
| ... | ... | |
| 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
|
| ... | ... | |
| 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 |
|
@zoom = (zoom > 0 && zoom < 5) ? zoom : 2
|
|
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]
|
| ... | ... | |
| 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 |
|
### Extracted from the HTML view/helpers
|
| 102 |
101 |
# Returns the number of rows that will be rendered on the Gantt chart
|
| 103 |
102 |
def number_of_rows
|
| 104 |
103 |
return @number_of_rows if @number_of_rows
|
| 105 |
|
|
| 106 |
|
rows = if @project
|
| 107 |
|
number_of_rows_on_project(@project)
|
| 108 |
|
else
|
| 109 |
|
Project.roots.visible.has_module('issue_tracking').inject(0) do |total, project|
|
| 110 |
|
total += number_of_rows_on_project(project)
|
| 111 |
|
end
|
| 112 |
|
end
|
| 113 |
|
|
|
104 |
|
|
105 |
rows = projects.inject(0) {|total, p| total += number_of_rows_on_project(p)}
|
| 114 |
106 |
rows > @max_rows ? @max_rows : rows
|
| 115 |
107 |
end
|
| 116 |
108 |
|
| 117 |
109 |
# Returns the number of rows that will be used to list a project on
|
| 118 |
110 |
# the Gantt chart. This will recurse for each subproject.
|
| 119 |
111 |
def number_of_rows_on_project(project)
|
| 120 |
|
# Remove the project requirement for Versions because it will
|
| 121 |
|
# restrict issues to only be on the current project. This
|
| 122 |
|
# ends up missing issues which are assigned to shared versions.
|
| 123 |
|
@query.project = nil if @query.project
|
|
112 |
return 0 unless projects.include?(project)
|
| 124 |
113 |
|
| 125 |
|
# One Root project
|
| 126 |
114 |
count = 1
|
| 127 |
|
# Issues without a Version
|
| 128 |
|
count += project.issues.for_gantt.without_version.with_query(@query).count
|
| 129 |
|
|
| 130 |
|
# Versions
|
| 131 |
|
count += project.versions.count
|
| 132 |
|
|
| 133 |
|
# Issues on the Versions
|
| 134 |
|
project.versions.each do |version|
|
| 135 |
|
count += version.fixed_issues.for_gantt.with_query(@query).count
|
| 136 |
|
end
|
| 137 |
|
|
| 138 |
|
# Subprojects
|
| 139 |
|
project.children.visible.has_module('issue_tracking').each do |subproject|
|
| 140 |
|
count += number_of_rows_on_project(subproject)
|
| 141 |
|
end
|
| 142 |
|
|
|
115 |
count += project_issues(project).size
|
|
116 |
count += project_versions(project).size
|
| 143 |
117 |
count
|
| 144 |
118 |
end
|
| 145 |
119 |
|
| ... | ... | |
| 154 |
128 |
render(options.merge(:only => :lines)) unless @lines_rendered
|
| 155 |
129 |
@lines
|
| 156 |
130 |
end
|
| 157 |
|
|
|
131 |
|
|
132 |
# Returns issues that will be rendered
|
|
133 |
def issues
|
|
134 |
@issues ||= @query.issues(
|
|
135 |
:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
|
|
136 |
:order => "#{Project.table_name}.lft ASC, #{Issue.table_name}.id ASC",
|
|
137 |
:limit => @max_rows
|
|
138 |
)
|
|
139 |
end
|
|
140 |
|
|
141 |
# Return all the project nodes that will be displayed
|
|
142 |
def projects
|
|
143 |
return @projects if @projects
|
|
144 |
|
|
145 |
ids = issues.collect(&:project).uniq.collect(&:id)
|
|
146 |
if ids.any?
|
|
147 |
# All issues projects and their visible ancestors
|
|
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",
|
|
150 |
:conditions => ["child.id IN (?)", ids],
|
|
151 |
:order => "#{Project.table_name}.lft ASC"
|
|
152 |
).uniq
|
|
153 |
else
|
|
154 |
@projects = []
|
|
155 |
end
|
|
156 |
end
|
|
157 |
|
|
158 |
# Returns the issues that belong to +project+
|
|
159 |
def project_issues(project)
|
|
160 |
@issues_by_project ||= issues.group_by(&:project)
|
|
161 |
@issues_by_project[project] || []
|
|
162 |
end
|
|
163 |
|
|
164 |
# Returns the distinct versions of the issues that belong to +project+
|
|
165 |
def project_versions(project)
|
|
166 |
project_issues(project).collect(&:fixed_version).compact.uniq
|
|
167 |
end
|
|
168 |
|
|
169 |
# Returns the issues that belong to +project+ and are assigned to +version+
|
|
170 |
def version_issues(project, version)
|
|
171 |
project_issues(project).select {|issue| issue.fixed_version == version}
|
|
172 |
end
|
|
173 |
|
| 158 |
174 |
def render(options={})
|
| 159 |
|
options = {:indent => 4, :render => :subject, :format => :html}.merge(options)
|
| 160 |
|
|
|
175 |
options = {:top => 0, :top_increment => 20, :indent_increment => 20, :render => :subject, :format => :html}.merge(options)
|
|
176 |
indent = options[:indent] || 4
|
|
177 |
|
| 161 |
178 |
@subjects = '' unless options[:only] == :lines
|
| 162 |
179 |
@lines = '' unless options[:only] == :subjects
|
| 163 |
180 |
@number_of_rows = 0
|
| 164 |
|
|
| 165 |
|
if @project
|
| 166 |
|
render_project(@project, options)
|
| 167 |
|
else
|
| 168 |
|
Project.roots.visible.has_module('issue_tracking').each do |project|
|
| 169 |
|
render_project(project, options)
|
| 170 |
|
break if abort?
|
| 171 |
|
end
|
|
181 |
|
|
182 |
Project.project_tree(projects) do |project, level|
|
|
183 |
options[:indent] = indent + level * options[:indent_increment]
|
|
184 |
render_project(project, options)
|
|
185 |
break if abort?
|
| 172 |
186 |
end
|
| 173 |
|
|
|
187 |
|
| 174 |
188 |
@subjects_rendered = true unless options[:only] == :lines
|
| 175 |
189 |
@lines_rendered = true unless options[:only] == :subjects
|
| 176 |
|
|
|
190 |
|
| 177 |
191 |
render_end(options)
|
| 178 |
192 |
end
|
| 179 |
193 |
|
| 180 |
194 |
def render_project(project, options={})
|
| 181 |
|
options[:top] = 0 unless options.key? :top
|
| 182 |
|
options[:indent_increment] = 20 unless options.key? :indent_increment
|
| 183 |
|
options[:top_increment] = 20 unless options.key? :top_increment
|
| 184 |
|
|
| 185 |
195 |
subject_for_project(project, options) unless options[:only] == :lines
|
| 186 |
196 |
line_for_project(project, options) unless options[:only] == :subjects
|
| 187 |
|
|
|
197 |
|
| 188 |
198 |
options[:top] += options[:top_increment]
|
| 189 |
199 |
options[:indent] += options[:indent_increment]
|
| 190 |
200 |
@number_of_rows += 1
|
| 191 |
201 |
return if abort?
|
| 192 |
|
|
| 193 |
|
# Second, Issues without a version
|
| 194 |
|
issues = project.issues.for_gantt.without_version.with_query(@query).all(:limit => current_limit)
|
|
202 |
|
|
203 |
issues = project_issues(project).select {|i| i.fixed_version.nil?}
|
| 195 |
204 |
sort_issues!(issues)
|
| 196 |
205 |
if issues
|
| 197 |
206 |
render_issues(issues, options)
|
| 198 |
207 |
return if abort?
|
| 199 |
208 |
end
|
| 200 |
209 |
|
| 201 |
|
# Third, Versions
|
| 202 |
|
project.versions.sort.each do |version|
|
| 203 |
|
render_version(version, options)
|
| 204 |
|
return if abort?
|
|
210 |
versions = project_versions(project)
|
|
211 |
versions.each do |version|
|
|
212 |
render_version(project, version, options)
|
| 205 |
213 |
end
|
| 206 |
214 |
|
| 207 |
|
# Fourth, subprojects
|
| 208 |
|
project.children.visible.has_module('issue_tracking').each do |project|
|
| 209 |
|
render_project(project, options)
|
| 210 |
|
return if abort?
|
| 211 |
|
end unless project.leaf?
|
| 212 |
|
|
| 213 |
215 |
# Remove indent to hit the next sibling
|
| 214 |
216 |
options[:indent] -= options[:indent_increment]
|
| 215 |
217 |
end
|
| 216 |
218 |
|
| 217 |
219 |
def render_issues(issues, options={})
|
| 218 |
220 |
@issue_ancestors = []
|
| 219 |
|
|
|
221 |
|
| 220 |
222 |
issues.each do |i|
|
| 221 |
223 |
subject_for_issue(i, options) unless options[:only] == :lines
|
| 222 |
224 |
line_for_issue(i, options) unless options[:only] == :subjects
|
| 223 |
|
|
|
225 |
|
| 224 |
226 |
options[:top] += options[:top_increment]
|
| 225 |
227 |
@number_of_rows += 1
|
| 226 |
228 |
break if abort?
|
| 227 |
229 |
end
|
| 228 |
|
|
|
230 |
|
| 229 |
231 |
options[:indent] -= (options[:indent_increment] * @issue_ancestors.size)
|
| 230 |
232 |
end
|
| 231 |
233 |
|
| 232 |
|
def render_version(version, options={})
|
|
234 |
def render_version(project, version, options={})
|
| 233 |
235 |
# Version header
|
| 234 |
236 |
subject_for_version(version, options) unless options[:only] == :lines
|
| 235 |
237 |
line_for_version(version, options) unless options[:only] == :subjects
|
| 236 |
|
|
|
238 |
|
| 237 |
239 |
options[:top] += options[:top_increment]
|
| 238 |
240 |
@number_of_rows += 1
|
| 239 |
241 |
return if abort?
|
| 240 |
|
|
| 241 |
|
# Remove the project requirement for Versions because it will
|
| 242 |
|
# restrict issues to only be on the current project. This
|
| 243 |
|
# ends up missing issues which are assigned to shared versions.
|
| 244 |
|
@query.project = nil if @query.project
|
| 245 |
|
|
| 246 |
|
issues = version.fixed_issues.for_gantt.with_query(@query).all(:limit => current_limit)
|
|
242 |
|
|
243 |
issues = version_issues(project, version)
|
| 247 |
244 |
if issues
|
| 248 |
245 |
sort_issues!(issues)
|
| 249 |
246 |
# Indent issues
|
| ... | ... | |
| 252 |
249 |
options[:indent] -= options[:indent_increment]
|
| 253 |
250 |
end
|
| 254 |
251 |
end
|
| 255 |
|
|
|
252 |
|
| 256 |
253 |
def render_end(options={})
|
| 257 |
254 |
case options[:format]
|
| 258 |
|
when :pdf
|
|
255 |
when :pdf
|
| 259 |
256 |
options[:pdf].Line(15, options[:top], PDF::TotalWidth, options[:top])
|
| 260 |
257 |
end
|
| 261 |
258 |
end
|
| ... | ... | |
| 280 |
277 |
if project.is_a?(Project) && project.start_date && project.due_date
|
| 281 |
278 |
options[:zoom] ||= 1
|
| 282 |
279 |
options[:g_width] ||= (self.date_to - self.date_from + 1) * options[:zoom]
|
| 283 |
|
|
|
280 |
|
| 284 |
281 |
coords = coordinates(project.start_date, project.due_date, nil, options[:zoom])
|
| 285 |
282 |
label = h(project)
|
| 286 |
|
|
|
283 |
|
| 287 |
284 |
case options[:format]
|
| 288 |
285 |
when :html
|
| 289 |
286 |
html_task(options, coords, :css => "project task", :label => label, :markers => true)
|
| ... | ... | |
| 318 |
315 |
if version.is_a?(Version) && version.start_date && version.due_date
|
| 319 |
316 |
options[:zoom] ||= 1
|
| 320 |
317 |
options[:g_width] ||= (self.date_to - self.date_from + 1) * options[:zoom]
|
| 321 |
|
|
|
318 |
|
| 322 |
319 |
coords = coordinates(version.start_date, version.due_date, version.completed_pourcent, options[:zoom])
|
| 323 |
320 |
label = "#{h version } #{h version.completed_pourcent.to_i.to_s}%"
|
| 324 |
321 |
label = h("#{version.project} -") + label unless @project && @project == version.project
|
| ... | ... | |
| 342 |
339 |
@issue_ancestors.pop
|
| 343 |
340 |
options[:indent] -= options[:indent_increment]
|
| 344 |
341 |
end
|
| 345 |
|
|
|
342 |
|
| 346 |
343 |
output = case options[:format]
|
| 347 |
344 |
when :html
|
| 348 |
345 |
css_classes = ''
|
| 349 |
346 |
css_classes << ' issue-overdue' if issue.overdue?
|
| 350 |
347 |
css_classes << ' issue-behind-schedule' if issue.behind_schedule?
|
| 351 |
348 |
css_classes << ' icon icon-issue' unless Setting.gravatar_enabled? && issue.assigned_to
|
| 352 |
|
|
|
349 |
|
| 353 |
350 |
subject = "<span class='#{css_classes}'>"
|
| 354 |
351 |
if issue.assigned_to.present?
|
| 355 |
352 |
assigned_string = l(:field_assigned_to) + ": " + issue.assigned_to.name
|
| ... | ... | |
| 369 |
366 |
@issue_ancestors << issue
|
| 370 |
367 |
options[:indent] += options[:indent_increment]
|
| 371 |
368 |
end
|
| 372 |
|
|
|
369 |
|
| 373 |
370 |
output
|
| 374 |
371 |
end
|
| 375 |
372 |
|
| ... | ... | |
| 378 |
375 |
if issue.is_a?(Issue) && issue.due_before
|
| 379 |
376 |
coords = coordinates(issue.start_date, issue.due_before, issue.done_ratio, options[:zoom])
|
| 380 |
377 |
label = "#{ issue.status.name } #{ issue.done_ratio }%"
|
| 381 |
|
|
|
378 |
|
| 382 |
379 |
case options[:format]
|
| 383 |
380 |
when :html
|
| 384 |
381 |
html_task(options, coords, :css => "task " + (issue.leaf? ? 'leaf' : 'parent'), :label => label, :issue => issue, :markers => !issue.leaf?)
|
| ... | ... | |
| 396 |
393 |
# Generates a gantt image
|
| 397 |
394 |
# Only defined if RMagick is avalaible
|
| 398 |
395 |
def to_image(format='PNG')
|
| 399 |
|
date_to = (@date_from >> @months)-1
|
|
396 |
date_to = (@date_from >> @months)-1
|
| 400 |
397 |
show_weeks = @zoom > 1
|
| 401 |
398 |
show_days = @zoom > 2
|
| 402 |
|
|
|
399 |
|
| 403 |
400 |
subject_width = 400
|
| 404 |
|
header_heigth = 18
|
|
401 |
header_height = 18
|
| 405 |
402 |
# width of one day in pixels
|
| 406 |
403 |
zoom = @zoom*2
|
| 407 |
404 |
g_width = (@date_to - @date_from + 1)*zoom
|
| 408 |
405 |
g_height = 20 * number_of_rows + 30
|
| 409 |
|
headers_heigth = (show_weeks ? 2*header_heigth : header_heigth)
|
| 410 |
|
height = g_height + headers_heigth
|
| 411 |
|
|
|
406 |
headers_height = (show_weeks ? 2*header_height : header_height)
|
|
407 |
height = g_height + headers_height
|
|
408 |
|
| 412 |
409 |
imgl = Magick::ImageList.new
|
| 413 |
410 |
imgl.new_image(subject_width+g_width+1, height)
|
| 414 |
411 |
gc = Magick::Draw.new
|
| 415 |
|
|
|
412 |
|
| 416 |
413 |
# Subjects
|
| 417 |
414 |
gc.stroke('transparent')
|
| 418 |
|
subjects(:image => gc, :top => (headers_heigth + 20), :indent => 4, :format => :image)
|
| 419 |
|
|
|
415 |
subjects(:image => gc, :top => (headers_height + 20), :indent => 4, :format => :image)
|
|
416 |
|
| 420 |
417 |
# Months headers
|
| 421 |
418 |
month_f = @date_from
|
| 422 |
419 |
left = subject_width
|
| 423 |
|
@months.times do
|
|
420 |
@months.times do
|
| 424 |
421 |
width = ((month_f >> 1) - month_f) * zoom
|
| 425 |
422 |
gc.fill('white')
|
| 426 |
423 |
gc.stroke('grey')
|
| ... | ... | |
| 433 |
430 |
left = left + width
|
| 434 |
431 |
month_f = month_f >> 1
|
| 435 |
432 |
end
|
| 436 |
|
|
|
433 |
|
| 437 |
434 |
# Weeks headers
|
| 438 |
435 |
if show_weeks
|
| 439 |
436 |
left = subject_width
|
| 440 |
|
height = header_heigth
|
|
437 |
height = header_height
|
| 441 |
438 |
if @date_from.cwday == 1
|
| 442 |
439 |
# date_from is monday
|
| 443 |
440 |
week_f = date_from
|
| ... | ... | |
| 448 |
445 |
gc.fill('white')
|
| 449 |
446 |
gc.stroke('grey')
|
| 450 |
447 |
gc.stroke_width(1)
|
| 451 |
|
gc.rectangle(left, header_heigth, left + width, 2*header_heigth + g_height-1)
|
|
448 |
gc.rectangle(left, header_height, left + width, 2*header_height + g_height-1)
|
| 452 |
449 |
left = left + width
|
| 453 |
450 |
end
|
| 454 |
451 |
while week_f <= date_to
|
| ... | ... | |
| 456 |
453 |
gc.fill('white')
|
| 457 |
454 |
gc.stroke('grey')
|
| 458 |
455 |
gc.stroke_width(1)
|
| 459 |
|
gc.rectangle(left.round, header_heigth, left.round + width, 2*header_heigth + g_height-1)
|
|
456 |
gc.rectangle(left.round, header_height, left.round + width, 2*header_height + g_height-1)
|
| 460 |
457 |
gc.fill('black')
|
| 461 |
458 |
gc.stroke('transparent')
|
| 462 |
459 |
gc.stroke_width(1)
|
| 463 |
|
gc.text(left.round + 2, header_heigth + 14, week_f.cweek.to_s)
|
|
460 |
gc.text(left.round + 2, header_height + 14, week_f.cweek.to_s)
|
| 464 |
461 |
left = left + width
|
| 465 |
462 |
week_f = week_f+7
|
| 466 |
463 |
end
|
| 467 |
464 |
end
|
| 468 |
|
|
|
465 |
|
| 469 |
466 |
# Days details (week-end in grey)
|
| 470 |
467 |
if show_days
|
| 471 |
468 |
left = subject_width
|
| 472 |
|
height = g_height + header_heigth - 1
|
|
469 |
height = g_height + header_height - 1
|
| 473 |
470 |
wday = @date_from.cwday
|
| 474 |
|
(date_to - @date_from + 1).to_i.times do
|
|
471 |
(date_to - @date_from + 1).to_i.times do
|
| 475 |
472 |
width = zoom
|
| 476 |
473 |
gc.fill(wday == 6 || wday == 7 ? '#eee' : 'white')
|
| 477 |
474 |
gc.stroke('#ddd')
|
| 478 |
475 |
gc.stroke_width(1)
|
| 479 |
|
gc.rectangle(left, 2*header_heigth, left + width, 2*header_heigth + g_height-1)
|
|
476 |
gc.rectangle(left, 2*header_height, left + width, 2*header_height + g_height-1)
|
| 480 |
477 |
left = left + width
|
| 481 |
478 |
wday = wday + 1
|
| 482 |
479 |
wday = 1 if wday > 7
|
| 483 |
480 |
end
|
| 484 |
481 |
end
|
| 485 |
|
|
|
482 |
|
| 486 |
483 |
# border
|
| 487 |
484 |
gc.fill('transparent')
|
| 488 |
485 |
gc.stroke('grey')
|
| 489 |
486 |
gc.stroke_width(1)
|
| 490 |
|
gc.rectangle(0, 0, subject_width+g_width, headers_heigth)
|
|
487 |
gc.rectangle(0, 0, subject_width+g_width, headers_height)
|
| 491 |
488 |
gc.stroke('black')
|
| 492 |
|
gc.rectangle(0, 0, subject_width+g_width, g_height+ headers_heigth-1)
|
| 493 |
|
|
|
489 |
gc.rectangle(0, 0, subject_width+g_width, g_height+ headers_height-1)
|
|
490 |
|
| 494 |
491 |
# content
|
| 495 |
|
top = headers_heigth + 20
|
|
492 |
top = headers_height + 20
|
| 496 |
493 |
|
| 497 |
494 |
gc.stroke('transparent')
|
| 498 |
495 |
lines(:image => gc, :top => top, :zoom => zoom, :subject_width => subject_width, :format => :image)
|
| 499 |
|
|
|
496 |
|
| 500 |
497 |
# today red line
|
| 501 |
498 |
if Date.today >= @date_from and Date.today <= date_to
|
| 502 |
499 |
gc.stroke('red')
|
| 503 |
500 |
x = (Date.today-@date_from+1)*zoom + subject_width
|
| 504 |
|
gc.line(x, headers_heigth, x, headers_heigth + g_height-1)
|
| 505 |
|
end
|
| 506 |
|
|
|
501 |
gc.line(x, headers_height, x, headers_height + g_height-1)
|
|
502 |
end
|
|
503 |
|
| 507 |
504 |
gc.draw(imgl)
|
| 508 |
505 |
imgl.format = format
|
| 509 |
506 |
imgl.to_blob
|
| 510 |
507 |
end if Object.const_defined?(:Magick)
|
| 511 |
508 |
|
| 512 |
509 |
def to_pdf
|
| 513 |
|
pdf = ::Redmine::Export::PDF::IFPDF.new(current_language)
|
|
510 |
pdf = ::Redmine::Export::PDF::ITCPDF.new(current_language)
|
| 514 |
511 |
pdf.SetTitle("#{l(:label_gantt)} #{project}")
|
| 515 |
|
pdf.AliasNbPages
|
|
512 |
pdf.alias_nb_pages
|
| 516 |
513 |
pdf.footer_date = format_date(Date.today)
|
| 517 |
514 |
pdf.AddPage("L")
|
| 518 |
515 |
pdf.SetFontStyle('B',12)
|
| 519 |
516 |
pdf.SetX(15)
|
| 520 |
|
pdf.Cell(PDF::LeftPaneWidth, 20, project.to_s)
|
|
517 |
pdf.RDMCell(PDF::LeftPaneWidth, 20, project.to_s)
|
| 521 |
518 |
pdf.Ln
|
| 522 |
519 |
pdf.SetFontStyle('B',9)
|
| 523 |
|
|
|
520 |
|
| 524 |
521 |
subject_width = PDF::LeftPaneWidth
|
| 525 |
|
header_heigth = 5
|
| 526 |
|
|
| 527 |
|
headers_heigth = header_heigth
|
|
522 |
header_height = 5
|
|
523 |
|
|
524 |
headers_height = header_height
|
| 528 |
525 |
show_weeks = false
|
| 529 |
526 |
show_days = false
|
| 530 |
|
|
|
527 |
|
| 531 |
528 |
if self.months < 7
|
| 532 |
529 |
show_weeks = true
|
| 533 |
|
headers_heigth = 2*header_heigth
|
|
530 |
headers_height = 2*header_height
|
| 534 |
531 |
if self.months < 3
|
| 535 |
532 |
show_days = true
|
| 536 |
|
headers_heigth = 3*header_heigth
|
|
533 |
headers_height = 3*header_height
|
| 537 |
534 |
end
|
| 538 |
535 |
end
|
| 539 |
|
|
|
536 |
|
| 540 |
537 |
g_width = PDF.right_pane_width
|
| 541 |
538 |
zoom = (g_width) / (self.date_to - self.date_from + 1)
|
| 542 |
539 |
g_height = 120
|
| 543 |
|
t_height = g_height + headers_heigth
|
| 544 |
|
|
|
540 |
t_height = g_height + headers_height
|
|
541 |
|
| 545 |
542 |
y_start = pdf.GetY
|
| 546 |
|
|
|
543 |
|
| 547 |
544 |
# Months headers
|
| 548 |
545 |
month_f = self.date_from
|
| 549 |
546 |
left = subject_width
|
| 550 |
|
height = header_heigth
|
| 551 |
|
self.months.times do
|
| 552 |
|
width = ((month_f >> 1) - month_f) * zoom
|
|
547 |
height = header_height
|
|
548 |
self.months.times do
|
|
549 |
width = ((month_f >> 1) - month_f) * zoom
|
| 553 |
550 |
pdf.SetY(y_start)
|
| 554 |
551 |
pdf.SetX(left)
|
| 555 |
|
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")
|
| 556 |
553 |
left = left + width
|
| 557 |
554 |
month_f = month_f >> 1
|
| 558 |
|
end
|
| 559 |
|
|
|
555 |
end
|
|
556 |
|
| 560 |
557 |
# Weeks headers
|
| 561 |
558 |
if show_weeks
|
| 562 |
559 |
left = subject_width
|
| 563 |
|
height = header_heigth
|
|
560 |
height = header_height
|
| 564 |
561 |
if self.date_from.cwday == 1
|
| 565 |
562 |
# self.date_from is monday
|
| 566 |
563 |
week_f = self.date_from
|
| ... | ... | |
| 568 |
565 |
# find next monday after self.date_from
|
| 569 |
566 |
week_f = self.date_from + (7 - self.date_from.cwday + 1)
|
| 570 |
567 |
width = (7 - self.date_from.cwday + 1) * zoom-1
|
| 571 |
|
pdf.SetY(y_start + header_heigth)
|
|
568 |
pdf.SetY(y_start + header_height)
|
| 572 |
569 |
pdf.SetX(left)
|
| 573 |
|
pdf.Cell(width + 1, height, "", "LTR")
|
|
570 |
pdf.RDMCell(width + 1, height, "", "LTR")
|
| 574 |
571 |
left = left + width+1
|
| 575 |
572 |
end
|
| 576 |
573 |
while week_f <= self.date_to
|
| 577 |
574 |
width = (week_f + 6 <= self.date_to) ? 7 * zoom : (self.date_to - week_f + 1) * zoom
|
| 578 |
|
pdf.SetY(y_start + header_heigth)
|
|
575 |
pdf.SetY(y_start + header_height)
|
| 579 |
576 |
pdf.SetX(left)
|
| 580 |
|
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")
|
| 581 |
578 |
left = left + width
|
| 582 |
579 |
week_f = week_f+7
|
| 583 |
580 |
end
|
| 584 |
581 |
end
|
| 585 |
|
|
|
582 |
|
| 586 |
583 |
# Days headers
|
| 587 |
584 |
if show_days
|
| 588 |
585 |
left = subject_width
|
| 589 |
|
height = header_heigth
|
|
586 |
height = header_height
|
| 590 |
587 |
wday = self.date_from.cwday
|
| 591 |
588 |
pdf.SetFontStyle('B',7)
|
| 592 |
|
(self.date_to - self.date_from + 1).to_i.times do
|
|
589 |
(self.date_to - self.date_from + 1).to_i.times do
|
| 593 |
590 |
width = zoom
|
| 594 |
|
pdf.SetY(y_start + 2 * header_heigth)
|
|
591 |
pdf.SetY(y_start + 2 * header_height)
|
| 595 |
592 |
pdf.SetX(left)
|
| 596 |
|
pdf.Cell(width, height, day_name(wday).first, "LTR", 0, "C")
|
|
593 |
pdf.RDMCell(width, height, day_name(wday).first, "LTR", 0, "C")
|
| 597 |
594 |
left = left + width
|
| 598 |
595 |
wday = wday + 1
|
| 599 |
596 |
wday = 1 if wday > 7
|
| 600 |
597 |
end
|
| 601 |
598 |
end
|
| 602 |
|
|
|
599 |
|
| 603 |
600 |
pdf.SetY(y_start)
|
| 604 |
601 |
pdf.SetX(15)
|
| 605 |
|
pdf.Cell(subject_width+g_width-15, headers_heigth, "", 1)
|
| 606 |
|
|
|
602 |
pdf.RDMCell(subject_width+g_width-15, headers_height, "", 1)
|
|
603 |
|
| 607 |
604 |
# Tasks
|
| 608 |
|
top = headers_heigth + y_start
|
|
605 |
top = headers_height + y_start
|
| 609 |
606 |
options = {
|
| 610 |
607 |
:top => top,
|
| 611 |
608 |
:zoom => zoom,
|
| ... | ... | |
| 620 |
617 |
render(options)
|
| 621 |
618 |
pdf.Output
|
| 622 |
619 |
end
|
| 623 |
|
|
|
620 |
|
| 624 |
621 |
private
|
| 625 |
|
|
|
622 |
|
| 626 |
623 |
def coordinates(start_date, end_date, progress, zoom=nil)
|
| 627 |
624 |
zoom ||= @zoom
|
| 628 |
|
|
|
625 |
|
| 629 |
626 |
coords = {}
|
| 630 |
627 |
if start_date && end_date && start_date < self.date_to && end_date > self.date_from
|
| 631 |
628 |
if start_date > self.date_from
|
| ... | ... | |
| 640 |
637 |
else
|
| 641 |
638 |
coords[:bar_end] = self.date_to - self.date_from + 1
|
| 642 |
639 |
end
|
| 643 |
|
|
|
640 |
|
| 644 |
641 |
if progress
|
| 645 |
|
progress_date = start_date + (end_date - start_date) * (progress / 100.0)
|
|
642 |
progress_date = start_date + (end_date - start_date + 1) * (progress / 100.0)
|
| 646 |
643 |
if progress_date > self.date_from && progress_date > start_date
|
| 647 |
644 |
if progress_date < self.date_to
|
| 648 |
|
coords[:bar_progress_end] = progress_date - self.date_from + 1
|
|
645 |
coords[:bar_progress_end] = progress_date - self.date_from
|
| 649 |
646 |
else
|
| 650 |
647 |
coords[:bar_progress_end] = self.date_to - self.date_from + 1
|
| 651 |
648 |
end
|
| 652 |
649 |
end
|
| 653 |
|
|
|
650 |
|
| 654 |
651 |
if progress_date < Date.today
|
| 655 |
652 |
late_date = [Date.today, end_date].min
|
| 656 |
653 |
if late_date > self.date_from && late_date > start_date
|
| ... | ... | |
| 663 |
660 |
end
|
| 664 |
661 |
end
|
| 665 |
662 |
end
|
| 666 |
|
|
|
663 |
|
| 667 |
664 |
# Transforms dates into pixels witdh
|
| 668 |
665 |
coords.keys.each do |key|
|
| 669 |
666 |
coords[key] = (coords[key] * zoom).floor
|
| ... | ... | |
| 675 |
672 |
def sort_issues!(issues)
|
| 676 |
673 |
issues.sort! { |a, b| gantt_issue_compare(a, b, issues) }
|
| 677 |
674 |
end
|
| 678 |
|
|
|
675 |
|
| 679 |
676 |
# TODO: top level issues should be sorted by start date
|
| 680 |
677 |
def gantt_issue_compare(x, y, issues)
|
| 681 |
678 |
if x.root_id == y.root_id
|
| ... | ... | |
| 684 |
681 |
x.root_id <=> y.root_id
|
| 685 |
682 |
end
|
| 686 |
683 |
end
|
| 687 |
|
|
|
684 |
|
| 688 |
685 |
def current_limit
|
| 689 |
686 |
if @max_rows
|
| 690 |
687 |
@max_rows - @number_of_rows
|
| ... | ... | |
| 692 |
689 |
nil
|
| 693 |
690 |
end
|
| 694 |
691 |
end
|
| 695 |
|
|
|
692 |
|
| 696 |
693 |
def abort?
|
| 697 |
694 |
if @max_rows && @number_of_rows >= @max_rows
|
| 698 |
695 |
@truncated = true
|
| 699 |
696 |
end
|
| 700 |
697 |
end
|
| 701 |
|
|
|
698 |
|
| 702 |
699 |
def pdf_new_page?(options)
|
| 703 |
700 |
if options[:top] > 180
|
| 704 |
701 |
options[:pdf].Line(15, options[:top], PDF::TotalWidth, options[:top])
|
| ... | ... | |
| 707 |
704 |
options[:pdf].Line(15, options[:top] - 0.1, PDF::TotalWidth, options[:top] - 0.1)
|
| 708 |
705 |
end
|
| 709 |
706 |
end
|
| 710 |
|
|
|
707 |
|
| 711 |
708 |
def html_subject(params, subject, options={})
|
| 712 |
709 |
style = "position: absolute;top:#{params[:top]}px;left:#{params[:indent]}px;"
|
| 713 |
710 |
style << "width:#{params[:subject_width] - params[:indent]}px;" if params[:subject_width]
|
| 714 |
|
|
|
711 |
|
| 715 |
712 |
output = view.content_tag 'div', subject, :class => options[:css], :style => style, :title => options[:title]
|
| 716 |
713 |
@subjects << output
|
| 717 |
714 |
output
|
| 718 |
715 |
end
|
| 719 |
|
|
|
716 |
|
| 720 |
717 |
def pdf_subject(params, subject, options={})
|
| 721 |
718 |
params[:pdf].SetY(params[:top])
|
| 722 |
719 |
params[:pdf].SetX(15)
|
| 723 |
|
|
|
720 |
|
| 724 |
721 |
char_limit = PDF::MaxCharactorsForSubject - params[:indent]
|
| 725 |
|
params[:pdf].Cell(params[:subject_width]-15, 5, (" " * params[:indent]) + subject.to_s.sub(/^(.{#{char_limit}}[^\s]*\s).*$/, '\1 (...)'), "LR")
|
| 726 |
|
|
|
722 |
params[:pdf].RDMCell(params[:subject_width]-15, 5, (" " * params[:indent]) + subject.to_s.sub(/^(.{#{char_limit}}[^\s]*\s).*$/, '\1 (...)'), "LR")
|
|
723 |
|
| 727 |
724 |
params[:pdf].SetY(params[:top])
|
| 728 |
725 |
params[:pdf].SetX(params[:subject_width])
|
| 729 |
|
params[:pdf].Cell(params[:g_width], 5, "", "LR")
|
|
726 |
params[:pdf].RDMCell(params[:g_width], 5, "", "LR")
|
| 730 |
727 |
end
|
| 731 |
|
|
|
728 |
|
| 732 |
729 |
def image_subject(params, subject, options={})
|
| 733 |
730 |
params[:image].fill('black')
|
| 734 |
731 |
params[:image].stroke('transparent')
|
| 735 |
732 |
params[:image].stroke_width(1)
|
| 736 |
733 |
params[:image].text(params[:indent], params[:top] + 2, subject)
|
| 737 |
734 |
end
|
| 738 |
|
|
|
735 |
|
| 739 |
736 |
def html_task(params, coords, options={})
|
| 740 |
737 |
output = ''
|
| 741 |
738 |
# Renders the task bar, with progress and late
|
| 742 |
739 |
if coords[:bar_start] && coords[:bar_end]
|
| 743 |
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'> </div>"
|
| 744 |
|
|
|
741 |
|
| 745 |
742 |
if coords[:bar_late_end]
|
| 746 |
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'> </div>"
|
| 747 |
744 |
end
|
| ... | ... | |
| 774 |
771 |
@lines << output
|
| 775 |
772 |
output
|
| 776 |
773 |
end
|
| 777 |
|
|
|
774 |
|
| 778 |
775 |
def pdf_task(params, coords, options={})
|
| 779 |
776 |
height = options[:height] || 2
|
| 780 |
|
|
|
777 |
|
| 781 |
778 |
# Renders the task bar, with progress and late
|
| 782 |
779 |
if coords[:bar_start] && coords[:bar_end]
|
| 783 |
780 |
params[:pdf].SetY(params[:top]+1.5)
|
| 784 |
781 |
params[:pdf].SetX(params[:subject_width] + coords[:bar_start])
|
| 785 |
782 |
params[:pdf].SetFillColor(200,200,200)
|
| 786 |
|
params[:pdf].Cell(coords[:bar_end] - coords[:bar_start], height, "", 0, 0, "", 1)
|
| 787 |
|
|
|
783 |
params[:pdf].RDMCell(coords[:bar_end] - coords[:bar_start], height, "", 0, 0, "", 1)
|
|
784 |
|
| 788 |
785 |
if coords[:bar_late_end]
|
| 789 |
786 |
params[:pdf].SetY(params[:top]+1.5)
|
| 790 |
787 |
params[:pdf].SetX(params[:subject_width] + coords[:bar_start])
|
| 791 |
788 |
params[:pdf].SetFillColor(255,100,100)
|
| 792 |
|
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)
|
| 793 |
790 |
end
|
| 794 |
791 |
if coords[:bar_progress_end]
|
| 795 |
792 |
params[:pdf].SetY(params[:top]+1.5)
|
| 796 |
793 |
params[:pdf].SetX(params[:subject_width] + coords[:bar_start])
|
| 797 |
794 |
params[:pdf].SetFillColor(90,200,90)
|
| 798 |
|
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)
|
| 799 |
796 |
end
|
| 800 |
797 |
end
|
| 801 |
798 |
# Renders the markers
|
| ... | ... | |
| 804 |
801 |
params[:pdf].SetY(params[:top] + 1)
|
| 805 |
802 |
params[:pdf].SetX(params[:subject_width] + coords[:start] - 1)
|
| 806 |
803 |
params[:pdf].SetFillColor(50,50,200)
|
| 807 |
|
params[:pdf].Cell(2, 2, "", 0, 0, "", 1)
|
|
804 |
params[:pdf].RDMCell(2, 2, "", 0, 0, "", 1)
|
| 808 |
805 |
end
|
| 809 |
806 |
if coords[:end]
|
| 810 |
807 |
params[:pdf].SetY(params[:top] + 1)
|
| 811 |
808 |
params[:pdf].SetX(params[:subject_width] + coords[:end] - 1)
|
| 812 |
809 |
params[:pdf].SetFillColor(50,50,200)
|
| 813 |
|
params[:pdf].Cell(2, 2, "", 0, 0, "", 1)
|
|
810 |
params[:pdf].RDMCell(2, 2, "", 0, 0, "", 1)
|
| 814 |
811 |
end
|
| 815 |
812 |
end
|
| 816 |
813 |
# Renders the label on the right
|
| 817 |
814 |
if options[:label]
|
| 818 |
815 |
params[:pdf].SetX(params[:subject_width] + (coords[:bar_end] || 0) + 5)
|
| 819 |
|
params[:pdf].Cell(30, 2, options[:label])
|
|
816 |
params[:pdf].RDMCell(30, 2, options[:label])
|
| 820 |
817 |
end
|
| 821 |
818 |
end
|
| 822 |
819 |
|
| 823 |
820 |
def image_task(params, coords, options={})
|
| 824 |
821 |
height = options[:height] || 6
|
| 825 |
|
|
|
822 |
|
| 826 |
823 |
# Renders the task bar, with progress and late
|
| 827 |
824 |
if coords[:bar_start] && coords[:bar_end]
|
| 828 |
825 |
params[:image].fill('#aaa')
|
| 829 |
826 |
params[:image].rectangle(params[:subject_width] + coords[:bar_start], params[:top], params[:subject_width] + coords[:bar_end], params[:top] - height)
|
| 830 |
|
|
|
827 |
|
| 831 |
828 |
if coords[:bar_late_end]
|
| 832 |
829 |
params[:image].fill('#f66')
|
| 833 |
830 |
params[:image].rectangle(params[:subject_width] + coords[:bar_start], params[:top], params[:subject_width] + coords[:bar_late_end], params[:top] - height)
|