Chris@0
|
1 # Redmine - project management software
|
Chris@1494
|
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
|
Chris@0
|
3 #
|
Chris@0
|
4 # This program is free software; you can redistribute it and/or
|
Chris@0
|
5 # modify it under the terms of the GNU General Public License
|
Chris@0
|
6 # as published by the Free Software Foundation; either version 2
|
Chris@0
|
7 # of the License, or (at your option) any later version.
|
Chris@441
|
8 #
|
Chris@0
|
9 # This program is distributed in the hope that it will be useful,
|
Chris@0
|
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
|
Chris@0
|
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
Chris@0
|
12 # GNU General Public License for more details.
|
Chris@441
|
13 #
|
Chris@0
|
14 # You should have received a copy of the GNU General Public License
|
Chris@0
|
15 # along with this program; if not, write to the Free Software
|
Chris@0
|
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
Chris@0
|
17
|
Chris@0
|
18 module Redmine
|
Chris@0
|
19 module Helpers
|
Chris@0
|
20 # Simple class to handle gantt chart data
|
Chris@0
|
21 class Gantt
|
chris@22
|
22 include ERB::Util
|
chris@22
|
23 include Redmine::I18n
|
Chris@1115
|
24 include Redmine::Utils::DateCalculation
|
chris@22
|
25
|
Chris@1464
|
26 # Relation types that are rendered
|
Chris@1464
|
27 DRAW_TYPES = {
|
Chris@1464
|
28 IssueRelation::TYPE_BLOCKS => { :landscape_margin => 16, :color => '#F34F4F' },
|
Chris@1464
|
29 IssueRelation::TYPE_PRECEDES => { :landscape_margin => 20, :color => '#628FEA' }
|
Chris@1464
|
30 }.freeze
|
Chris@1464
|
31
|
chris@22
|
32 # :nodoc:
|
chris@22
|
33 # Some utility methods for the PDF export
|
chris@22
|
34 class PDF
|
chris@22
|
35 MaxCharactorsForSubject = 45
|
chris@22
|
36 TotalWidth = 280
|
chris@22
|
37 LeftPaneWidth = 100
|
chris@22
|
38
|
chris@22
|
39 def self.right_pane_width
|
chris@22
|
40 TotalWidth - LeftPaneWidth
|
chris@22
|
41 end
|
chris@22
|
42 end
|
chris@22
|
43
|
Chris@119
|
44 attr_reader :year_from, :month_from, :date_from, :date_to, :zoom, :months, :truncated, :max_rows
|
chris@22
|
45 attr_accessor :query
|
chris@22
|
46 attr_accessor :project
|
chris@22
|
47 attr_accessor :view
|
Chris@441
|
48
|
Chris@0
|
49 def initialize(options={})
|
Chris@0
|
50 options = options.dup
|
Chris@0
|
51 if options[:year] && options[:year].to_i >0
|
Chris@0
|
52 @year_from = options[:year].to_i
|
Chris@0
|
53 if options[:month] && options[:month].to_i >=1 && options[:month].to_i <= 12
|
Chris@0
|
54 @month_from = options[:month].to_i
|
Chris@0
|
55 else
|
Chris@0
|
56 @month_from = 1
|
Chris@0
|
57 end
|
Chris@0
|
58 else
|
Chris@0
|
59 @month_from ||= Date.today.month
|
Chris@0
|
60 @year_from ||= Date.today.year
|
Chris@0
|
61 end
|
Chris@0
|
62 zoom = (options[:zoom] || User.current.pref[:gantt_zoom]).to_i
|
Chris@441
|
63 @zoom = (zoom > 0 && zoom < 5) ? zoom : 2
|
Chris@0
|
64 months = (options[:months] || User.current.pref[:gantt_months]).to_i
|
Chris@0
|
65 @months = (months > 0 && months < 25) ? months : 6
|
Chris@0
|
66 # Save gantt parameters as user preference (zoom and months count)
|
Chris@1115
|
67 if (User.current.logged? && (@zoom != User.current.pref[:gantt_zoom] ||
|
Chris@1115
|
68 @months != User.current.pref[:gantt_months]))
|
Chris@0
|
69 User.current.pref[:gantt_zoom], User.current.pref[:gantt_months] = @zoom, @months
|
Chris@0
|
70 User.current.preference.save
|
Chris@0
|
71 end
|
Chris@0
|
72 @date_from = Date.civil(@year_from, @month_from, 1)
|
Chris@0
|
73 @date_to = (@date_from >> @months) - 1
|
Chris@119
|
74 @subjects = ''
|
Chris@119
|
75 @lines = ''
|
Chris@119
|
76 @number_of_rows = nil
|
Chris@119
|
77 @issue_ancestors = []
|
Chris@119
|
78 @truncated = false
|
Chris@119
|
79 if options.has_key?(:max_rows)
|
Chris@119
|
80 @max_rows = options[:max_rows]
|
Chris@119
|
81 else
|
Chris@119
|
82 @max_rows = Setting.gantt_items_limit.blank? ? nil : Setting.gantt_items_limit.to_i
|
Chris@119
|
83 end
|
Chris@0
|
84 end
|
chris@22
|
85
|
chris@22
|
86 def common_params
|
chris@22
|
87 { :controller => 'gantts', :action => 'show', :project_id => @project }
|
Chris@0
|
88 end
|
Chris@441
|
89
|
Chris@0
|
90 def params
|
Chris@1115
|
91 common_params.merge({:zoom => zoom, :year => year_from,
|
Chris@1115
|
92 :month => month_from, :months => months})
|
Chris@0
|
93 end
|
Chris@441
|
94
|
Chris@0
|
95 def params_previous
|
Chris@1115
|
96 common_params.merge({:year => (date_from << months).year,
|
Chris@1115
|
97 :month => (date_from << months).month,
|
Chris@1115
|
98 :zoom => zoom, :months => months})
|
Chris@0
|
99 end
|
Chris@441
|
100
|
Chris@0
|
101 def params_next
|
Chris@1115
|
102 common_params.merge({:year => (date_from >> months).year,
|
Chris@1115
|
103 :month => (date_from >> months).month,
|
Chris@1115
|
104 :zoom => zoom, :months => months})
|
Chris@0
|
105 end
|
chris@22
|
106
|
chris@22
|
107 # Returns the number of rows that will be rendered on the Gantt chart
|
chris@22
|
108 def number_of_rows
|
Chris@119
|
109 return @number_of_rows if @number_of_rows
|
Chris@441
|
110 rows = projects.inject(0) {|total, p| total += number_of_rows_on_project(p)}
|
Chris@119
|
111 rows > @max_rows ? @max_rows : rows
|
chris@22
|
112 end
|
chris@22
|
113
|
chris@22
|
114 # Returns the number of rows that will be used to list a project on
|
chris@22
|
115 # the Gantt chart. This will recurse for each subproject.
|
chris@22
|
116 def number_of_rows_on_project(project)
|
Chris@441
|
117 return 0 unless projects.include?(project)
|
chris@22
|
118 count = 1
|
Chris@441
|
119 count += project_issues(project).size
|
Chris@441
|
120 count += project_versions(project).size
|
chris@22
|
121 count
|
chris@22
|
122 end
|
chris@22
|
123
|
chris@22
|
124 # Renders the subjects of the Gantt chart, the left side.
|
chris@22
|
125 def subjects(options={})
|
Chris@119
|
126 render(options.merge(:only => :subjects)) unless @subjects_rendered
|
Chris@119
|
127 @subjects
|
chris@22
|
128 end
|
chris@22
|
129
|
chris@22
|
130 # Renders the lines of the Gantt chart, the right side
|
chris@22
|
131 def lines(options={})
|
Chris@119
|
132 render(options.merge(:only => :lines)) unless @lines_rendered
|
Chris@119
|
133 @lines
|
Chris@119
|
134 end
|
Chris@441
|
135
|
Chris@441
|
136 # Returns issues that will be rendered
|
Chris@441
|
137 def issues
|
Chris@441
|
138 @issues ||= @query.issues(
|
Chris@441
|
139 :include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
|
Chris@441
|
140 :order => "#{Project.table_name}.lft ASC, #{Issue.table_name}.id ASC",
|
Chris@441
|
141 :limit => @max_rows
|
Chris@441
|
142 )
|
Chris@441
|
143 end
|
Chris@441
|
144
|
Chris@1464
|
145 # Returns a hash of the relations between the issues that are present on the gantt
|
Chris@1464
|
146 # and that should be displayed, grouped by issue ids.
|
Chris@1464
|
147 def relations
|
Chris@1464
|
148 return @relations if @relations
|
Chris@1464
|
149 if issues.any?
|
Chris@1464
|
150 issue_ids = issues.map(&:id)
|
Chris@1464
|
151 @relations = IssueRelation.
|
Chris@1464
|
152 where(:issue_from_id => issue_ids, :issue_to_id => issue_ids, :relation_type => DRAW_TYPES.keys).
|
Chris@1464
|
153 group_by(&:issue_from_id)
|
Chris@1464
|
154 else
|
Chris@1464
|
155 @relations = {}
|
Chris@1464
|
156 end
|
Chris@1464
|
157 end
|
Chris@1464
|
158
|
Chris@441
|
159 # Return all the project nodes that will be displayed
|
Chris@441
|
160 def projects
|
Chris@441
|
161 return @projects if @projects
|
Chris@441
|
162 ids = issues.collect(&:project).uniq.collect(&:id)
|
Chris@441
|
163 if ids.any?
|
Chris@441
|
164 # All issues projects and their visible ancestors
|
Chris@1464
|
165 @projects = Project.visible.
|
Chris@1464
|
166 joins("LEFT JOIN #{Project.table_name} child ON #{Project.table_name}.lft <= child.lft AND #{Project.table_name}.rgt >= child.rgt").
|
Chris@1464
|
167 where("child.id IN (?)", ids).
|
Chris@1464
|
168 order("#{Project.table_name}.lft ASC").
|
Chris@1464
|
169 uniq.
|
Chris@1464
|
170 all
|
Chris@441
|
171 else
|
Chris@441
|
172 @projects = []
|
Chris@441
|
173 end
|
Chris@441
|
174 end
|
Chris@441
|
175
|
Chris@441
|
176 # Returns the issues that belong to +project+
|
Chris@441
|
177 def project_issues(project)
|
Chris@441
|
178 @issues_by_project ||= issues.group_by(&:project)
|
Chris@441
|
179 @issues_by_project[project] || []
|
Chris@441
|
180 end
|
Chris@441
|
181
|
Chris@441
|
182 # Returns the distinct versions of the issues that belong to +project+
|
Chris@441
|
183 def project_versions(project)
|
Chris@441
|
184 project_issues(project).collect(&:fixed_version).compact.uniq
|
Chris@441
|
185 end
|
Chris@441
|
186
|
Chris@441
|
187 # Returns the issues that belong to +project+ and are assigned to +version+
|
Chris@441
|
188 def version_issues(project, version)
|
Chris@441
|
189 project_issues(project).select {|issue| issue.fixed_version == version}
|
Chris@441
|
190 end
|
Chris@441
|
191
|
Chris@119
|
192 def render(options={})
|
Chris@1115
|
193 options = {:top => 0, :top_increment => 20,
|
Chris@1115
|
194 :indent_increment => 20, :render => :subject,
|
Chris@1115
|
195 :format => :html}.merge(options)
|
Chris@441
|
196 indent = options[:indent] || 4
|
Chris@119
|
197 @subjects = '' unless options[:only] == :lines
|
Chris@119
|
198 @lines = '' unless options[:only] == :subjects
|
Chris@119
|
199 @number_of_rows = 0
|
Chris@441
|
200 Project.project_tree(projects) do |project, level|
|
Chris@441
|
201 options[:indent] = indent + level * options[:indent_increment]
|
Chris@441
|
202 render_project(project, options)
|
Chris@441
|
203 break if abort?
|
chris@22
|
204 end
|
Chris@119
|
205 @subjects_rendered = true unless options[:only] == :lines
|
Chris@119
|
206 @lines_rendered = true unless options[:only] == :subjects
|
Chris@119
|
207 render_end(options)
|
chris@22
|
208 end
|
chris@22
|
209
|
chris@22
|
210 def render_project(project, options={})
|
Chris@119
|
211 subject_for_project(project, options) unless options[:only] == :lines
|
Chris@119
|
212 line_for_project(project, options) unless options[:only] == :subjects
|
chris@22
|
213 options[:top] += options[:top_increment]
|
chris@22
|
214 options[:indent] += options[:indent_increment]
|
Chris@119
|
215 @number_of_rows += 1
|
Chris@119
|
216 return if abort?
|
Chris@441
|
217 issues = project_issues(project).select {|i| i.fixed_version.nil?}
|
Chris@1464
|
218 self.class.sort_issues!(issues)
|
chris@22
|
219 if issues
|
Chris@119
|
220 render_issues(issues, options)
|
Chris@119
|
221 return if abort?
|
chris@22
|
222 end
|
Chris@441
|
223 versions = project_versions(project)
|
Chris@1464
|
224 self.class.sort_versions!(versions)
|
Chris@441
|
225 versions.each do |version|
|
Chris@441
|
226 render_version(project, version, options)
|
chris@22
|
227 end
|
chris@22
|
228 # Remove indent to hit the next sibling
|
chris@22
|
229 options[:indent] -= options[:indent_increment]
|
chris@22
|
230 end
|
chris@22
|
231
|
chris@22
|
232 def render_issues(issues, options={})
|
Chris@119
|
233 @issue_ancestors = []
|
chris@22
|
234 issues.each do |i|
|
Chris@119
|
235 subject_for_issue(i, options) unless options[:only] == :lines
|
Chris@119
|
236 line_for_issue(i, options) unless options[:only] == :subjects
|
chris@22
|
237 options[:top] += options[:top_increment]
|
Chris@119
|
238 @number_of_rows += 1
|
Chris@119
|
239 break if abort?
|
chris@22
|
240 end
|
Chris@119
|
241 options[:indent] -= (options[:indent_increment] * @issue_ancestors.size)
|
chris@22
|
242 end
|
chris@22
|
243
|
Chris@441
|
244 def render_version(project, version, options={})
|
chris@22
|
245 # Version header
|
Chris@119
|
246 subject_for_version(version, options) unless options[:only] == :lines
|
Chris@119
|
247 line_for_version(version, options) unless options[:only] == :subjects
|
chris@22
|
248 options[:top] += options[:top_increment]
|
Chris@119
|
249 @number_of_rows += 1
|
Chris@119
|
250 return if abort?
|
Chris@441
|
251 issues = version_issues(project, version)
|
chris@22
|
252 if issues
|
Chris@1464
|
253 self.class.sort_issues!(issues)
|
chris@22
|
254 # Indent issues
|
chris@22
|
255 options[:indent] += options[:indent_increment]
|
Chris@119
|
256 render_issues(issues, options)
|
chris@22
|
257 options[:indent] -= options[:indent_increment]
|
chris@22
|
258 end
|
Chris@119
|
259 end
|
Chris@441
|
260
|
Chris@119
|
261 def render_end(options={})
|
Chris@119
|
262 case options[:format]
|
Chris@441
|
263 when :pdf
|
Chris@119
|
264 options[:pdf].Line(15, options[:top], PDF::TotalWidth, options[:top])
|
Chris@119
|
265 end
|
chris@22
|
266 end
|
chris@22
|
267
|
chris@22
|
268 def subject_for_project(project, options)
|
chris@22
|
269 case options[:format]
|
chris@22
|
270 when :html
|
Chris@1115
|
271 html_class = ""
|
Chris@1115
|
272 html_class << 'icon icon-projects '
|
Chris@1115
|
273 html_class << (project.overdue? ? 'project-overdue' : '')
|
Chris@1115
|
274 s = view.link_to_project(project).html_safe
|
Chris@1115
|
275 subject = view.content_tag(:span, s,
|
Chris@1115
|
276 :class => html_class).html_safe
|
Chris@119
|
277 html_subject(options, subject, :css => "project-name")
|
chris@22
|
278 when :image
|
Chris@119
|
279 image_subject(options, project.name)
|
chris@22
|
280 when :pdf
|
Chris@119
|
281 pdf_new_page?(options)
|
Chris@119
|
282 pdf_subject(options, project.name)
|
chris@22
|
283 end
|
chris@22
|
284 end
|
chris@22
|
285
|
chris@22
|
286 def line_for_project(project, options)
|
chris@37
|
287 # Skip versions that don't have a start_date or due date
|
chris@37
|
288 if project.is_a?(Project) && project.start_date && project.due_date
|
chris@22
|
289 options[:zoom] ||= 1
|
chris@22
|
290 options[:g_width] ||= (self.date_to - self.date_from + 1) * options[:zoom]
|
Chris@119
|
291 coords = coordinates(project.start_date, project.due_date, nil, options[:zoom])
|
Chris@119
|
292 label = h(project)
|
chris@22
|
293 case options[:format]
|
chris@22
|
294 when :html
|
Chris@119
|
295 html_task(options, coords, :css => "project task", :label => label, :markers => true)
|
chris@22
|
296 when :image
|
Chris@119
|
297 image_task(options, coords, :label => label, :markers => true, :height => 3)
|
chris@22
|
298 when :pdf
|
Chris@119
|
299 pdf_task(options, coords, :label => label, :markers => true, :height => 0.8)
|
chris@22
|
300 end
|
chris@22
|
301 else
|
chris@22
|
302 ''
|
chris@22
|
303 end
|
chris@22
|
304 end
|
chris@22
|
305
|
chris@22
|
306 def subject_for_version(version, options)
|
chris@22
|
307 case options[:format]
|
chris@22
|
308 when :html
|
Chris@1115
|
309 html_class = ""
|
Chris@1115
|
310 html_class << 'icon icon-package '
|
Chris@1115
|
311 html_class << (version.behind_schedule? ? 'version-behind-schedule' : '') << " "
|
Chris@1115
|
312 html_class << (version.overdue? ? 'version-overdue' : '')
|
Chris@1464
|
313 html_class << ' version-closed' unless version.open?
|
Chris@1464
|
314 if version.start_date && version.due_date && version.completed_pourcent
|
Chris@1464
|
315 progress_date = calc_progress_date(version.start_date,
|
Chris@1464
|
316 version.due_date, version.completed_pourcent)
|
Chris@1464
|
317 html_class << ' behind-start-date' if progress_date < self.date_from
|
Chris@1464
|
318 html_class << ' over-end-date' if progress_date > self.date_to
|
Chris@1464
|
319 end
|
Chris@1115
|
320 s = view.link_to_version(version).html_safe
|
Chris@1115
|
321 subject = view.content_tag(:span, s,
|
Chris@1115
|
322 :class => html_class).html_safe
|
Chris@1464
|
323 html_subject(options, subject, :css => "version-name",
|
Chris@1464
|
324 :id => "version-#{version.id}")
|
chris@22
|
325 when :image
|
Chris@119
|
326 image_subject(options, version.to_s_with_project)
|
chris@22
|
327 when :pdf
|
Chris@119
|
328 pdf_new_page?(options)
|
Chris@119
|
329 pdf_subject(options, version.to_s_with_project)
|
chris@22
|
330 end
|
chris@22
|
331 end
|
chris@22
|
332
|
chris@22
|
333 def line_for_version(version, options)
|
chris@22
|
334 # Skip versions that don't have a start_date
|
Chris@1464
|
335 if version.is_a?(Version) && version.due_date && version.start_date
|
chris@22
|
336 options[:zoom] ||= 1
|
chris@22
|
337 options[:g_width] ||= (self.date_to - self.date_from + 1) * options[:zoom]
|
Chris@1115
|
338 coords = coordinates(version.start_date,
|
Chris@1464
|
339 version.due_date, version.completed_percent,
|
Chris@1115
|
340 options[:zoom])
|
Chris@1464
|
341 label = "#{h version} #{h version.completed_percent.to_i.to_s}%"
|
Chris@119
|
342 label = h("#{version.project} -") + label unless @project && @project == version.project
|
chris@22
|
343 case options[:format]
|
chris@22
|
344 when :html
|
Chris@1464
|
345 html_task(options, coords, :css => "version task",
|
Chris@1464
|
346 :label => label, :markers => true, :version => version)
|
chris@22
|
347 when :image
|
Chris@119
|
348 image_task(options, coords, :label => label, :markers => true, :height => 3)
|
chris@22
|
349 when :pdf
|
Chris@119
|
350 pdf_task(options, coords, :label => label, :markers => true, :height => 0.8)
|
chris@22
|
351 end
|
chris@22
|
352 else
|
chris@22
|
353 ''
|
chris@22
|
354 end
|
chris@22
|
355 end
|
chris@22
|
356
|
chris@22
|
357 def subject_for_issue(issue, options)
|
Chris@119
|
358 while @issue_ancestors.any? && !issue.is_descendant_of?(@issue_ancestors.last)
|
Chris@119
|
359 @issue_ancestors.pop
|
Chris@119
|
360 options[:indent] -= options[:indent_increment]
|
Chris@119
|
361 end
|
Chris@119
|
362 output = case options[:format]
|
chris@22
|
363 when :html
|
Chris@119
|
364 css_classes = ''
|
Chris@119
|
365 css_classes << ' issue-overdue' if issue.overdue?
|
Chris@119
|
366 css_classes << ' issue-behind-schedule' if issue.behind_schedule?
|
Chris@119
|
367 css_classes << ' icon icon-issue' unless Setting.gravatar_enabled? && issue.assigned_to
|
Chris@1464
|
368 css_classes << ' issue-closed' if issue.closed?
|
Chris@1464
|
369 if issue.start_date && issue.due_before && issue.done_ratio
|
Chris@1464
|
370 progress_date = calc_progress_date(issue.start_date,
|
Chris@1464
|
371 issue.due_before, issue.done_ratio)
|
Chris@1464
|
372 css_classes << ' behind-start-date' if progress_date < self.date_from
|
Chris@1464
|
373 css_classes << ' over-end-date' if progress_date > self.date_to
|
Chris@1464
|
374 end
|
Chris@1115
|
375 s = "".html_safe
|
Chris@119
|
376 if issue.assigned_to.present?
|
Chris@119
|
377 assigned_string = l(:field_assigned_to) + ": " + issue.assigned_to.name
|
Chris@1115
|
378 s << view.avatar(issue.assigned_to,
|
Chris@1115
|
379 :class => 'gravatar icon-gravatar',
|
Chris@1115
|
380 :size => 10,
|
Chris@1115
|
381 :title => assigned_string).to_s.html_safe
|
Chris@119
|
382 end
|
Chris@1115
|
383 s << view.link_to_issue(issue).html_safe
|
Chris@1115
|
384 subject = view.content_tag(:span, s, :class => css_classes).html_safe
|
Chris@1115
|
385 html_subject(options, subject, :css => "issue-subject",
|
Chris@1464
|
386 :title => issue.subject, :id => "issue-#{issue.id}") + "\n"
|
Chris@119
|
387 when :image
|
Chris@119
|
388 image_subject(options, issue.subject)
|
Chris@119
|
389 when :pdf
|
Chris@119
|
390 pdf_new_page?(options)
|
Chris@119
|
391 pdf_subject(options, issue.subject)
|
Chris@119
|
392 end
|
Chris@119
|
393 unless issue.leaf?
|
Chris@119
|
394 @issue_ancestors << issue
|
Chris@119
|
395 options[:indent] += options[:indent_increment]
|
Chris@119
|
396 end
|
Chris@119
|
397 output
|
chris@22
|
398 end
|
chris@22
|
399
|
chris@22
|
400 def line_for_issue(issue, options)
|
chris@22
|
401 # Skip issues that don't have a due_before (due_date or version's due_date)
|
chris@22
|
402 if issue.is_a?(Issue) && issue.due_before
|
Chris@119
|
403 coords = coordinates(issue.start_date, issue.due_before, issue.done_ratio, options[:zoom])
|
Chris@1115
|
404 label = "#{issue.status.name} #{issue.done_ratio}%"
|
chris@22
|
405 case options[:format]
|
chris@22
|
406 when :html
|
Chris@1115
|
407 html_task(options, coords,
|
Chris@1115
|
408 :css => "task " + (issue.leaf? ? 'leaf' : 'parent'),
|
Chris@1115
|
409 :label => label, :issue => issue,
|
Chris@1115
|
410 :markers => !issue.leaf?)
|
chris@22
|
411 when :image
|
Chris@119
|
412 image_task(options, coords, :label => label)
|
chris@22
|
413 when :pdf
|
Chris@119
|
414 pdf_task(options, coords, :label => label)
|
Chris@119
|
415 end
|
chris@22
|
416 else
|
chris@22
|
417 ''
|
chris@22
|
418 end
|
chris@22
|
419 end
|
chris@22
|
420
|
Chris@0
|
421 # Generates a gantt image
|
Chris@0
|
422 # Only defined if RMagick is avalaible
|
chris@22
|
423 def to_image(format='PNG')
|
Chris@1115
|
424 date_to = (@date_from >> @months) - 1
|
Chris@0
|
425 show_weeks = @zoom > 1
|
Chris@0
|
426 show_days = @zoom > 2
|
Chris@1
|
427 subject_width = 400
|
Chris@441
|
428 header_height = 18
|
Chris@0
|
429 # width of one day in pixels
|
Chris@1115
|
430 zoom = @zoom * 2
|
Chris@1115
|
431 g_width = (@date_to - @date_from + 1) * zoom
|
chris@22
|
432 g_height = 20 * number_of_rows + 30
|
Chris@1115
|
433 headers_height = (show_weeks ? 2 * header_height : header_height)
|
Chris@441
|
434 height = g_height + headers_height
|
Chris@0
|
435 imgl = Magick::ImageList.new
|
Chris@1115
|
436 imgl.new_image(subject_width + g_width + 1, height)
|
Chris@0
|
437 gc = Magick::Draw.new
|
Chris@1115
|
438 gc.font = Redmine::Configuration['rmagick_font_path'] || ""
|
Chris@0
|
439 # Subjects
|
Chris@119
|
440 gc.stroke('transparent')
|
Chris@441
|
441 subjects(:image => gc, :top => (headers_height + 20), :indent => 4, :format => :image)
|
Chris@0
|
442 # Months headers
|
Chris@0
|
443 month_f = @date_from
|
Chris@0
|
444 left = subject_width
|
Chris@441
|
445 @months.times do
|
Chris@0
|
446 width = ((month_f >> 1) - month_f) * zoom
|
Chris@0
|
447 gc.fill('white')
|
Chris@0
|
448 gc.stroke('grey')
|
Chris@0
|
449 gc.stroke_width(1)
|
Chris@0
|
450 gc.rectangle(left, 0, left + width, height)
|
Chris@0
|
451 gc.fill('black')
|
Chris@0
|
452 gc.stroke('transparent')
|
Chris@0
|
453 gc.stroke_width(1)
|
Chris@0
|
454 gc.text(left.round + 8, 14, "#{month_f.year}-#{month_f.month}")
|
Chris@0
|
455 left = left + width
|
Chris@0
|
456 month_f = month_f >> 1
|
Chris@0
|
457 end
|
Chris@0
|
458 # Weeks headers
|
Chris@0
|
459 if show_weeks
|
Chris@1115
|
460 left = subject_width
|
Chris@1115
|
461 height = header_height
|
Chris@1115
|
462 if @date_from.cwday == 1
|
Chris@1115
|
463 # date_from is monday
|
Chris@1115
|
464 week_f = date_from
|
Chris@1115
|
465 else
|
Chris@1115
|
466 # find next monday after date_from
|
Chris@1115
|
467 week_f = @date_from + (7 - @date_from.cwday + 1)
|
Chris@1115
|
468 width = (7 - @date_from.cwday + 1) * zoom
|
Chris@1115
|
469 gc.fill('white')
|
Chris@1115
|
470 gc.stroke('grey')
|
Chris@1115
|
471 gc.stroke_width(1)
|
Chris@1115
|
472 gc.rectangle(left, header_height, left + width, 2 * header_height + g_height - 1)
|
Chris@1115
|
473 left = left + width
|
Chris@1115
|
474 end
|
Chris@1115
|
475 while week_f <= date_to
|
Chris@1115
|
476 width = (week_f + 6 <= date_to) ? 7 * zoom : (date_to - week_f + 1) * zoom
|
Chris@1115
|
477 gc.fill('white')
|
Chris@1115
|
478 gc.stroke('grey')
|
Chris@1115
|
479 gc.stroke_width(1)
|
Chris@1115
|
480 gc.rectangle(left.round, header_height, left.round + width, 2 * header_height + g_height - 1)
|
Chris@1115
|
481 gc.fill('black')
|
Chris@1115
|
482 gc.stroke('transparent')
|
Chris@1115
|
483 gc.stroke_width(1)
|
Chris@1115
|
484 gc.text(left.round + 2, header_height + 14, week_f.cweek.to_s)
|
Chris@1115
|
485 left = left + width
|
Chris@1115
|
486 week_f = week_f + 7
|
Chris@1115
|
487 end
|
Chris@0
|
488 end
|
Chris@0
|
489 # Days details (week-end in grey)
|
Chris@0
|
490 if show_days
|
Chris@1115
|
491 left = subject_width
|
Chris@1115
|
492 height = g_height + header_height - 1
|
Chris@1115
|
493 wday = @date_from.cwday
|
Chris@1115
|
494 (date_to - @date_from + 1).to_i.times do
|
Chris@1115
|
495 width = zoom
|
Chris@1115
|
496 gc.fill(non_working_week_days.include?(wday) ? '#eee' : 'white')
|
Chris@1115
|
497 gc.stroke('#ddd')
|
Chris@1115
|
498 gc.stroke_width(1)
|
Chris@1115
|
499 gc.rectangle(left, 2 * header_height, left + width, 2 * header_height + g_height - 1)
|
Chris@1115
|
500 left = left + width
|
Chris@1115
|
501 wday = wday + 1
|
Chris@1115
|
502 wday = 1 if wday > 7
|
Chris@1115
|
503 end
|
Chris@0
|
504 end
|
Chris@0
|
505 # border
|
Chris@0
|
506 gc.fill('transparent')
|
Chris@0
|
507 gc.stroke('grey')
|
Chris@0
|
508 gc.stroke_width(1)
|
Chris@1115
|
509 gc.rectangle(0, 0, subject_width + g_width, headers_height)
|
Chris@0
|
510 gc.stroke('black')
|
Chris@1115
|
511 gc.rectangle(0, 0, subject_width + g_width, g_height + headers_height - 1)
|
Chris@0
|
512 # content
|
Chris@441
|
513 top = headers_height + 20
|
Chris@119
|
514 gc.stroke('transparent')
|
Chris@1115
|
515 lines(:image => gc, :top => top, :zoom => zoom,
|
Chris@1115
|
516 :subject_width => subject_width, :format => :image)
|
Chris@0
|
517 # today red line
|
Chris@0
|
518 if Date.today >= @date_from and Date.today <= date_to
|
Chris@0
|
519 gc.stroke('red')
|
Chris@1115
|
520 x = (Date.today - @date_from + 1) * zoom + subject_width
|
Chris@1115
|
521 gc.line(x, headers_height, x, headers_height + g_height - 1)
|
Chris@441
|
522 end
|
Chris@0
|
523 gc.draw(imgl)
|
Chris@0
|
524 imgl.format = format
|
Chris@0
|
525 imgl.to_blob
|
Chris@0
|
526 end if Object.const_defined?(:Magick)
|
chris@22
|
527
|
chris@22
|
528 def to_pdf
|
Chris@441
|
529 pdf = ::Redmine::Export::PDF::ITCPDF.new(current_language)
|
chris@22
|
530 pdf.SetTitle("#{l(:label_gantt)} #{project}")
|
Chris@441
|
531 pdf.alias_nb_pages
|
chris@22
|
532 pdf.footer_date = format_date(Date.today)
|
chris@22
|
533 pdf.AddPage("L")
|
Chris@1115
|
534 pdf.SetFontStyle('B', 12)
|
chris@22
|
535 pdf.SetX(15)
|
Chris@441
|
536 pdf.RDMCell(PDF::LeftPaneWidth, 20, project.to_s)
|
chris@22
|
537 pdf.Ln
|
Chris@1115
|
538 pdf.SetFontStyle('B', 9)
|
chris@22
|
539 subject_width = PDF::LeftPaneWidth
|
Chris@441
|
540 header_height = 5
|
Chris@441
|
541 headers_height = header_height
|
chris@22
|
542 show_weeks = false
|
chris@22
|
543 show_days = false
|
chris@22
|
544 if self.months < 7
|
chris@22
|
545 show_weeks = true
|
Chris@1115
|
546 headers_height = 2 * header_height
|
chris@22
|
547 if self.months < 3
|
chris@22
|
548 show_days = true
|
Chris@1115
|
549 headers_height = 3 * header_height
|
chris@22
|
550 end
|
chris@22
|
551 end
|
chris@22
|
552 g_width = PDF.right_pane_width
|
chris@22
|
553 zoom = (g_width) / (self.date_to - self.date_from + 1)
|
chris@22
|
554 g_height = 120
|
Chris@441
|
555 t_height = g_height + headers_height
|
chris@22
|
556 y_start = pdf.GetY
|
chris@22
|
557 # Months headers
|
chris@22
|
558 month_f = self.date_from
|
chris@22
|
559 left = subject_width
|
Chris@441
|
560 height = header_height
|
Chris@441
|
561 self.months.times do
|
Chris@441
|
562 width = ((month_f >> 1) - month_f) * zoom
|
chris@22
|
563 pdf.SetY(y_start)
|
chris@22
|
564 pdf.SetX(left)
|
Chris@441
|
565 pdf.RDMCell(width, height, "#{month_f.year}-#{month_f.month}", "LTR", 0, "C")
|
chris@22
|
566 left = left + width
|
chris@22
|
567 month_f = month_f >> 1
|
Chris@441
|
568 end
|
chris@22
|
569 # Weeks headers
|
chris@22
|
570 if show_weeks
|
chris@22
|
571 left = subject_width
|
Chris@441
|
572 height = header_height
|
chris@22
|
573 if self.date_from.cwday == 1
|
chris@22
|
574 # self.date_from is monday
|
chris@22
|
575 week_f = self.date_from
|
chris@22
|
576 else
|
chris@22
|
577 # find next monday after self.date_from
|
chris@22
|
578 week_f = self.date_from + (7 - self.date_from.cwday + 1)
|
chris@22
|
579 width = (7 - self.date_from.cwday + 1) * zoom-1
|
Chris@441
|
580 pdf.SetY(y_start + header_height)
|
chris@22
|
581 pdf.SetX(left)
|
Chris@441
|
582 pdf.RDMCell(width + 1, height, "", "LTR")
|
Chris@1115
|
583 left = left + width + 1
|
chris@22
|
584 end
|
chris@22
|
585 while week_f <= self.date_to
|
chris@22
|
586 width = (week_f + 6 <= self.date_to) ? 7 * zoom : (self.date_to - week_f + 1) * zoom
|
Chris@441
|
587 pdf.SetY(y_start + header_height)
|
chris@22
|
588 pdf.SetX(left)
|
Chris@441
|
589 pdf.RDMCell(width, height, (width >= 5 ? week_f.cweek.to_s : ""), "LTR", 0, "C")
|
chris@22
|
590 left = left + width
|
Chris@1115
|
591 week_f = week_f + 7
|
chris@22
|
592 end
|
chris@22
|
593 end
|
chris@22
|
594 # Days headers
|
chris@22
|
595 if show_days
|
chris@22
|
596 left = subject_width
|
Chris@441
|
597 height = header_height
|
chris@22
|
598 wday = self.date_from.cwday
|
Chris@1115
|
599 pdf.SetFontStyle('B', 7)
|
Chris@441
|
600 (self.date_to - self.date_from + 1).to_i.times do
|
chris@22
|
601 width = zoom
|
Chris@441
|
602 pdf.SetY(y_start + 2 * header_height)
|
chris@22
|
603 pdf.SetX(left)
|
Chris@441
|
604 pdf.RDMCell(width, height, day_name(wday).first, "LTR", 0, "C")
|
chris@22
|
605 left = left + width
|
chris@22
|
606 wday = wday + 1
|
chris@22
|
607 wday = 1 if wday > 7
|
chris@22
|
608 end
|
chris@22
|
609 end
|
chris@22
|
610 pdf.SetY(y_start)
|
chris@22
|
611 pdf.SetX(15)
|
Chris@1115
|
612 pdf.RDMCell(subject_width + g_width - 15, headers_height, "", 1)
|
chris@22
|
613 # Tasks
|
Chris@441
|
614 top = headers_height + y_start
|
Chris@119
|
615 options = {
|
Chris@119
|
616 :top => top,
|
Chris@119
|
617 :zoom => zoom,
|
Chris@119
|
618 :subject_width => subject_width,
|
Chris@119
|
619 :g_width => g_width,
|
Chris@119
|
620 :indent => 0,
|
Chris@119
|
621 :indent_increment => 5,
|
Chris@119
|
622 :top_increment => 5,
|
Chris@119
|
623 :format => :pdf,
|
Chris@119
|
624 :pdf => pdf
|
Chris@119
|
625 }
|
Chris@119
|
626 render(options)
|
chris@22
|
627 pdf.Output
|
chris@22
|
628 end
|
Chris@441
|
629
|
Chris@0
|
630 private
|
Chris@441
|
631
|
Chris@119
|
632 def coordinates(start_date, end_date, progress, zoom=nil)
|
Chris@119
|
633 zoom ||= @zoom
|
Chris@119
|
634 coords = {}
|
Chris@119
|
635 if start_date && end_date && start_date < self.date_to && end_date > self.date_from
|
Chris@119
|
636 if start_date > self.date_from
|
Chris@119
|
637 coords[:start] = start_date - self.date_from
|
Chris@119
|
638 coords[:bar_start] = start_date - self.date_from
|
Chris@119
|
639 else
|
Chris@119
|
640 coords[:bar_start] = 0
|
Chris@119
|
641 end
|
Chris@119
|
642 if end_date < self.date_to
|
Chris@119
|
643 coords[:end] = end_date - self.date_from
|
Chris@119
|
644 coords[:bar_end] = end_date - self.date_from + 1
|
Chris@119
|
645 else
|
Chris@119
|
646 coords[:bar_end] = self.date_to - self.date_from + 1
|
Chris@119
|
647 end
|
Chris@119
|
648 if progress
|
Chris@1464
|
649 progress_date = calc_progress_date(start_date, end_date, progress)
|
Chris@119
|
650 if progress_date > self.date_from && progress_date > start_date
|
Chris@119
|
651 if progress_date < self.date_to
|
Chris@441
|
652 coords[:bar_progress_end] = progress_date - self.date_from
|
Chris@119
|
653 else
|
Chris@119
|
654 coords[:bar_progress_end] = self.date_to - self.date_from + 1
|
Chris@119
|
655 end
|
Chris@119
|
656 end
|
Chris@119
|
657 if progress_date < Date.today
|
Chris@119
|
658 late_date = [Date.today, end_date].min
|
Chris@119
|
659 if late_date > self.date_from && late_date > start_date
|
Chris@119
|
660 if late_date < self.date_to
|
Chris@119
|
661 coords[:bar_late_end] = late_date - self.date_from + 1
|
Chris@119
|
662 else
|
Chris@119
|
663 coords[:bar_late_end] = self.date_to - self.date_from + 1
|
Chris@119
|
664 end
|
Chris@119
|
665 end
|
Chris@119
|
666 end
|
Chris@119
|
667 end
|
Chris@119
|
668 end
|
Chris@119
|
669 # Transforms dates into pixels witdh
|
Chris@119
|
670 coords.keys.each do |key|
|
Chris@119
|
671 coords[key] = (coords[key] * zoom).floor
|
Chris@119
|
672 end
|
Chris@119
|
673 coords
|
Chris@119
|
674 end
|
chris@22
|
675
|
Chris@1464
|
676 def calc_progress_date(start_date, end_date, progress)
|
Chris@1464
|
677 start_date + (end_date - start_date + 1) * (progress / 100.0)
|
Chris@119
|
678 end
|
Chris@441
|
679
|
Chris@1464
|
680 def self.sort_issues!(issues)
|
Chris@1464
|
681 issues.sort! {|a, b| sort_issue_logic(a) <=> sort_issue_logic(b)}
|
Chris@1464
|
682 end
|
Chris@1464
|
683
|
Chris@1464
|
684 def self.sort_issue_logic(issue)
|
Chris@1464
|
685 julian_date = Date.new()
|
Chris@1464
|
686 ancesters_start_date = []
|
Chris@1464
|
687 current_issue = issue
|
Chris@1464
|
688 begin
|
Chris@1464
|
689 ancesters_start_date.unshift([current_issue.start_date || julian_date, current_issue.id])
|
Chris@1464
|
690 current_issue = current_issue.parent
|
Chris@1464
|
691 end while (current_issue)
|
Chris@1464
|
692 ancesters_start_date
|
Chris@1464
|
693 end
|
Chris@1464
|
694
|
Chris@1464
|
695 def self.sort_versions!(versions)
|
Chris@1464
|
696 versions.sort!
|
Chris@119
|
697 end
|
Chris@441
|
698
|
Chris@119
|
699 def current_limit
|
Chris@119
|
700 if @max_rows
|
Chris@119
|
701 @max_rows - @number_of_rows
|
Chris@119
|
702 else
|
Chris@119
|
703 nil
|
Chris@119
|
704 end
|
Chris@119
|
705 end
|
Chris@441
|
706
|
Chris@119
|
707 def abort?
|
Chris@119
|
708 if @max_rows && @number_of_rows >= @max_rows
|
Chris@119
|
709 @truncated = true
|
Chris@119
|
710 end
|
Chris@119
|
711 end
|
Chris@441
|
712
|
Chris@119
|
713 def pdf_new_page?(options)
|
Chris@119
|
714 if options[:top] > 180
|
Chris@119
|
715 options[:pdf].Line(15, options[:top], PDF::TotalWidth, options[:top])
|
Chris@119
|
716 options[:pdf].AddPage("L")
|
Chris@119
|
717 options[:top] = 15
|
Chris@119
|
718 options[:pdf].Line(15, options[:top] - 0.1, PDF::TotalWidth, options[:top] - 0.1)
|
Chris@119
|
719 end
|
Chris@119
|
720 end
|
Chris@441
|
721
|
Chris@119
|
722 def html_subject(params, subject, options={})
|
Chris@245
|
723 style = "position: absolute;top:#{params[:top]}px;left:#{params[:indent]}px;"
|
Chris@245
|
724 style << "width:#{params[:subject_width] - params[:indent]}px;" if params[:subject_width]
|
Chris@1464
|
725 output = view.content_tag(:div, subject,
|
Chris@1115
|
726 :class => options[:css], :style => style,
|
Chris@1464
|
727 :title => options[:title],
|
Chris@1464
|
728 :id => options[:id])
|
Chris@119
|
729 @subjects << output
|
Chris@119
|
730 output
|
Chris@119
|
731 end
|
Chris@441
|
732
|
Chris@119
|
733 def pdf_subject(params, subject, options={})
|
Chris@119
|
734 params[:pdf].SetY(params[:top])
|
Chris@119
|
735 params[:pdf].SetX(15)
|
Chris@119
|
736 char_limit = PDF::MaxCharactorsForSubject - params[:indent]
|
Chris@1115
|
737 params[:pdf].RDMCell(params[:subject_width] - 15, 5,
|
Chris@1115
|
738 (" " * params[:indent]) +
|
Chris@1115
|
739 subject.to_s.sub(/^(.{#{char_limit}}[^\s]*\s).*$/, '\1 (...)'),
|
Chris@1115
|
740 "LR")
|
Chris@119
|
741 params[:pdf].SetY(params[:top])
|
Chris@119
|
742 params[:pdf].SetX(params[:subject_width])
|
Chris@441
|
743 params[:pdf].RDMCell(params[:g_width], 5, "", "LR")
|
Chris@119
|
744 end
|
Chris@441
|
745
|
Chris@119
|
746 def image_subject(params, subject, options={})
|
Chris@119
|
747 params[:image].fill('black')
|
Chris@119
|
748 params[:image].stroke('transparent')
|
Chris@119
|
749 params[:image].stroke_width(1)
|
Chris@119
|
750 params[:image].text(params[:indent], params[:top] + 2, subject)
|
Chris@119
|
751 end
|
Chris@441
|
752
|
Chris@1464
|
753 def issue_relations(issue)
|
Chris@1464
|
754 rels = {}
|
Chris@1464
|
755 if relations[issue.id]
|
Chris@1464
|
756 relations[issue.id].each do |relation|
|
Chris@1464
|
757 (rels[relation.relation_type] ||= []) << relation.issue_to_id
|
Chris@1464
|
758 end
|
Chris@1464
|
759 end
|
Chris@1464
|
760 rels
|
Chris@1464
|
761 end
|
Chris@1464
|
762
|
Chris@119
|
763 def html_task(params, coords, options={})
|
Chris@119
|
764 output = ''
|
Chris@119
|
765 # Renders the task bar, with progress and late
|
Chris@119
|
766 if coords[:bar_start] && coords[:bar_end]
|
Chris@1115
|
767 width = coords[:bar_end] - coords[:bar_start] - 2
|
Chris@1115
|
768 style = ""
|
Chris@1115
|
769 style << "top:#{params[:top]}px;"
|
Chris@1115
|
770 style << "left:#{coords[:bar_start]}px;"
|
Chris@1115
|
771 style << "width:#{width}px;"
|
Chris@1464
|
772 html_id = "task-todo-issue-#{options[:issue].id}" if options[:issue]
|
Chris@1464
|
773 html_id = "task-todo-version-#{options[:version].id}" if options[:version]
|
Chris@1464
|
774 content_opt = {:style => style,
|
Chris@1464
|
775 :class => "#{options[:css]} task_todo",
|
Chris@1464
|
776 :id => html_id}
|
Chris@1464
|
777 if options[:issue]
|
Chris@1464
|
778 rels = issue_relations(options[:issue])
|
Chris@1464
|
779 if rels.present?
|
Chris@1464
|
780 content_opt[:data] = {"rels" => rels.to_json}
|
Chris@1464
|
781 end
|
Chris@1464
|
782 end
|
Chris@1464
|
783 output << view.content_tag(:div, ' '.html_safe, content_opt)
|
Chris@119
|
784 if coords[:bar_late_end]
|
Chris@1115
|
785 width = coords[:bar_late_end] - coords[:bar_start] - 2
|
Chris@1115
|
786 style = ""
|
Chris@1115
|
787 style << "top:#{params[:top]}px;"
|
Chris@1115
|
788 style << "left:#{coords[:bar_start]}px;"
|
Chris@1115
|
789 style << "width:#{width}px;"
|
Chris@1115
|
790 output << view.content_tag(:div, ' '.html_safe,
|
Chris@1115
|
791 :style => style,
|
Chris@1115
|
792 :class => "#{options[:css]} task_late")
|
Chris@0
|
793 end
|
Chris@119
|
794 if coords[:bar_progress_end]
|
Chris@1115
|
795 width = coords[:bar_progress_end] - coords[:bar_start] - 2
|
Chris@1115
|
796 style = ""
|
Chris@1115
|
797 style << "top:#{params[:top]}px;"
|
Chris@1115
|
798 style << "left:#{coords[:bar_start]}px;"
|
Chris@1115
|
799 style << "width:#{width}px;"
|
Chris@1464
|
800 html_id = "task-done-issue-#{options[:issue].id}" if options[:issue]
|
Chris@1464
|
801 html_id = "task-done-version-#{options[:version].id}" if options[:version]
|
Chris@1115
|
802 output << view.content_tag(:div, ' '.html_safe,
|
Chris@1115
|
803 :style => style,
|
Chris@1464
|
804 :class => "#{options[:css]} task_done",
|
Chris@1464
|
805 :id => html_id)
|
Chris@119
|
806 end
|
Chris@119
|
807 end
|
Chris@119
|
808 # Renders the markers
|
Chris@119
|
809 if options[:markers]
|
Chris@119
|
810 if coords[:start]
|
Chris@1115
|
811 style = ""
|
Chris@1115
|
812 style << "top:#{params[:top]}px;"
|
Chris@1115
|
813 style << "left:#{coords[:start]}px;"
|
Chris@1115
|
814 style << "width:15px;"
|
Chris@1115
|
815 output << view.content_tag(:div, ' '.html_safe,
|
Chris@1115
|
816 :style => style,
|
Chris@1115
|
817 :class => "#{options[:css]} marker starting")
|
Chris@119
|
818 end
|
Chris@119
|
819 if coords[:end]
|
Chris@1115
|
820 style = ""
|
Chris@1115
|
821 style << "top:#{params[:top]}px;"
|
Chris@1115
|
822 style << "left:#{coords[:end] + params[:zoom]}px;"
|
Chris@1115
|
823 style << "width:15px;"
|
Chris@1115
|
824 output << view.content_tag(:div, ' '.html_safe,
|
Chris@1115
|
825 :style => style,
|
Chris@1115
|
826 :class => "#{options[:css]} marker ending")
|
Chris@119
|
827 end
|
Chris@119
|
828 end
|
Chris@119
|
829 # Renders the label on the right
|
Chris@119
|
830 if options[:label]
|
Chris@1115
|
831 style = ""
|
Chris@1115
|
832 style << "top:#{params[:top]}px;"
|
Chris@1115
|
833 style << "left:#{(coords[:bar_end] || 0) + 8}px;"
|
Chris@1115
|
834 style << "width:15px;"
|
Chris@1115
|
835 output << view.content_tag(:div, options[:label],
|
Chris@1115
|
836 :style => style,
|
Chris@1115
|
837 :class => "#{options[:css]} label")
|
Chris@119
|
838 end
|
Chris@119
|
839 # Renders the tooltip
|
Chris@119
|
840 if options[:issue] && coords[:bar_start] && coords[:bar_end]
|
Chris@1115
|
841 s = view.content_tag(:span,
|
Chris@1115
|
842 view.render_issue_tooltip(options[:issue]).html_safe,
|
Chris@1115
|
843 :class => "tip")
|
Chris@1115
|
844 style = ""
|
Chris@1115
|
845 style << "position: absolute;"
|
Chris@1115
|
846 style << "top:#{params[:top]}px;"
|
Chris@1115
|
847 style << "left:#{coords[:bar_start]}px;"
|
Chris@1115
|
848 style << "width:#{coords[:bar_end] - coords[:bar_start]}px;"
|
Chris@1115
|
849 style << "height:12px;"
|
Chris@1115
|
850 output << view.content_tag(:div, s.html_safe,
|
Chris@1115
|
851 :style => style,
|
Chris@1115
|
852 :class => "tooltip")
|
Chris@119
|
853 end
|
Chris@119
|
854 @lines << output
|
Chris@119
|
855 output
|
Chris@119
|
856 end
|
Chris@441
|
857
|
Chris@119
|
858 def pdf_task(params, coords, options={})
|
Chris@119
|
859 height = options[:height] || 2
|
Chris@119
|
860 # Renders the task bar, with progress and late
|
Chris@119
|
861 if coords[:bar_start] && coords[:bar_end]
|
Chris@1115
|
862 params[:pdf].SetY(params[:top] + 1.5)
|
Chris@119
|
863 params[:pdf].SetX(params[:subject_width] + coords[:bar_start])
|
Chris@1115
|
864 params[:pdf].SetFillColor(200, 200, 200)
|
Chris@441
|
865 params[:pdf].RDMCell(coords[:bar_end] - coords[:bar_start], height, "", 0, 0, "", 1)
|
Chris@119
|
866 if coords[:bar_late_end]
|
Chris@1115
|
867 params[:pdf].SetY(params[:top] + 1.5)
|
Chris@119
|
868 params[:pdf].SetX(params[:subject_width] + coords[:bar_start])
|
Chris@1115
|
869 params[:pdf].SetFillColor(255, 100, 100)
|
Chris@441
|
870 params[:pdf].RDMCell(coords[:bar_late_end] - coords[:bar_start], height, "", 0, 0, "", 1)
|
Chris@119
|
871 end
|
Chris@119
|
872 if coords[:bar_progress_end]
|
Chris@1115
|
873 params[:pdf].SetY(params[:top] + 1.5)
|
Chris@119
|
874 params[:pdf].SetX(params[:subject_width] + coords[:bar_start])
|
Chris@1115
|
875 params[:pdf].SetFillColor(90, 200, 90)
|
Chris@441
|
876 params[:pdf].RDMCell(coords[:bar_progress_end] - coords[:bar_start], height, "", 0, 0, "", 1)
|
Chris@119
|
877 end
|
Chris@119
|
878 end
|
Chris@119
|
879 # Renders the markers
|
Chris@119
|
880 if options[:markers]
|
Chris@119
|
881 if coords[:start]
|
Chris@119
|
882 params[:pdf].SetY(params[:top] + 1)
|
Chris@119
|
883 params[:pdf].SetX(params[:subject_width] + coords[:start] - 1)
|
Chris@1115
|
884 params[:pdf].SetFillColor(50, 50, 200)
|
Chris@441
|
885 params[:pdf].RDMCell(2, 2, "", 0, 0, "", 1)
|
Chris@119
|
886 end
|
Chris@119
|
887 if coords[:end]
|
Chris@119
|
888 params[:pdf].SetY(params[:top] + 1)
|
Chris@119
|
889 params[:pdf].SetX(params[:subject_width] + coords[:end] - 1)
|
Chris@1115
|
890 params[:pdf].SetFillColor(50, 50, 200)
|
Chris@441
|
891 params[:pdf].RDMCell(2, 2, "", 0, 0, "", 1)
|
Chris@119
|
892 end
|
Chris@119
|
893 end
|
Chris@119
|
894 # Renders the label on the right
|
Chris@119
|
895 if options[:label]
|
Chris@119
|
896 params[:pdf].SetX(params[:subject_width] + (coords[:bar_end] || 0) + 5)
|
Chris@441
|
897 params[:pdf].RDMCell(30, 2, options[:label])
|
Chris@0
|
898 end
|
Chris@0
|
899 end
|
chris@22
|
900
|
Chris@119
|
901 def image_task(params, coords, options={})
|
Chris@119
|
902 height = options[:height] || 6
|
Chris@119
|
903 # Renders the task bar, with progress and late
|
Chris@119
|
904 if coords[:bar_start] && coords[:bar_end]
|
Chris@119
|
905 params[:image].fill('#aaa')
|
Chris@1115
|
906 params[:image].rectangle(params[:subject_width] + coords[:bar_start],
|
Chris@1115
|
907 params[:top],
|
Chris@1115
|
908 params[:subject_width] + coords[:bar_end],
|
Chris@1115
|
909 params[:top] - height)
|
Chris@119
|
910 if coords[:bar_late_end]
|
Chris@119
|
911 params[:image].fill('#f66')
|
Chris@1115
|
912 params[:image].rectangle(params[:subject_width] + coords[:bar_start],
|
Chris@1115
|
913 params[:top],
|
Chris@1115
|
914 params[:subject_width] + coords[:bar_late_end],
|
Chris@1115
|
915 params[:top] - height)
|
Chris@119
|
916 end
|
Chris@119
|
917 if coords[:bar_progress_end]
|
Chris@119
|
918 params[:image].fill('#00c600')
|
Chris@1115
|
919 params[:image].rectangle(params[:subject_width] + coords[:bar_start],
|
Chris@1115
|
920 params[:top],
|
Chris@1115
|
921 params[:subject_width] + coords[:bar_progress_end],
|
Chris@1115
|
922 params[:top] - height)
|
Chris@119
|
923 end
|
Chris@119
|
924 end
|
Chris@119
|
925 # Renders the markers
|
Chris@119
|
926 if options[:markers]
|
Chris@119
|
927 if coords[:start]
|
Chris@119
|
928 x = params[:subject_width] + coords[:start]
|
Chris@119
|
929 y = params[:top] - height / 2
|
Chris@119
|
930 params[:image].fill('blue')
|
Chris@1115
|
931 params[:image].polygon(x - 4, y, x, y - 4, x + 4, y, x, y + 4)
|
Chris@119
|
932 end
|
Chris@119
|
933 if coords[:end]
|
Chris@119
|
934 x = params[:subject_width] + coords[:end] + params[:zoom]
|
Chris@119
|
935 y = params[:top] - height / 2
|
Chris@119
|
936 params[:image].fill('blue')
|
Chris@1115
|
937 params[:image].polygon(x - 4, y, x, y - 4, x + 4, y, x, y + 4)
|
Chris@119
|
938 end
|
Chris@119
|
939 end
|
Chris@119
|
940 # Renders the label on the right
|
Chris@119
|
941 if options[:label]
|
Chris@119
|
942 params[:image].fill('black')
|
Chris@1115
|
943 params[:image].text(params[:subject_width] + (coords[:bar_end] || 0) + 5,
|
Chris@1115
|
944 params[:top] + 1,
|
Chris@1115
|
945 options[:label])
|
Chris@119
|
946 end
|
Chris@119
|
947 end
|
Chris@0
|
948 end
|
Chris@0
|
949 end
|
Chris@0
|
950 end
|