1
|
# Redmine - project management software
|
2
|
# Copyright (C) 2006-2008 Jean-Philippe Lang
|
3
|
#
|
4
|
# This program is free software; you can redistribute it and/or
|
5
|
# modify it under the terms of the GNU General Public License
|
6
|
# as published by the Free Software Foundation; either version 2
|
7
|
# of the License, or (at your option) any later version.
|
8
|
#
|
9
|
# This program is distributed in the hope that it will be useful,
|
10
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
11
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
12
|
# GNU General Public License for more details.
|
13
|
#
|
14
|
# You should have received a copy of the GNU General Public License
|
15
|
# along with this program; if not, write to the Free Software
|
16
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
17
|
|
18
|
module Redmine
|
19
|
module Helpers
|
20
|
# Simple class to handle gantt chart data
|
21
|
class Gantt
|
22
|
include ERB::Util
|
23
|
include Redmine::I18n
|
24
|
|
25
|
# :nodoc:
|
26
|
# Some utility methods for the PDF export
|
27
|
class PDF
|
28
|
MaxCharactorsForSubject = 45
|
29
|
TotalWidth = 280
|
30
|
LeftPaneWidth = 100
|
31
|
|
32
|
def self.right_pane_width
|
33
|
TotalWidth - LeftPaneWidth
|
34
|
end
|
35
|
end
|
36
|
|
37
|
attr_reader :year_from, :month_from, :date_from, :date_to, :zoom, :months, :truncated, :max_rows
|
38
|
attr_accessor :query
|
39
|
attr_accessor :project
|
40
|
attr_accessor :view
|
41
|
|
42
|
def initialize(options={})
|
43
|
options = options.dup
|
44
|
|
45
|
if options[:year] && options[:year].to_i >0
|
46
|
@year_from = options[:year].to_i
|
47
|
if options[:month] && options[:month].to_i >=1 && options[:month].to_i <= 12
|
48
|
@month_from = options[:month].to_i
|
49
|
else
|
50
|
@month_from = 1
|
51
|
end
|
52
|
else
|
53
|
@month_from ||= Date.today.month
|
54
|
@year_from ||= Date.today.year
|
55
|
end
|
56
|
|
57
|
zoom = (options[:zoom] || User.current.pref[:gantt_zoom]).to_i
|
58
|
@zoom = (zoom > 0 && zoom < 5) ? zoom : 2
|
59
|
months = (options[:months] || User.current.pref[:gantt_months]).to_i
|
60
|
@months = (months > 0 && months < 25) ? months : 6
|
61
|
|
62
|
# Save gantt parameters as user preference (zoom and months count)
|
63
|
if (User.current.logged? && (@zoom != User.current.pref[:gantt_zoom] || @months != User.current.pref[:gantt_months]))
|
64
|
User.current.pref[:gantt_zoom], User.current.pref[:gantt_months] = @zoom, @months
|
65
|
User.current.preference.save
|
66
|
end
|
67
|
|
68
|
@date_from = Date.civil(@year_from, @month_from, 1)
|
69
|
@date_to = (@date_from >> @months) - 1
|
70
|
|
71
|
@subjects = ''
|
72
|
@lines = ''
|
73
|
@calendars = ''
|
74
|
@number_of_rows = nil
|
75
|
|
76
|
@issue_ancestors = []
|
77
|
|
78
|
@truncated = false
|
79
|
if options.has_key?(:max_rows)
|
80
|
@max_rows = options[:max_rows]
|
81
|
else
|
82
|
@max_rows = Setting.gantt_items_limit.blank? ? nil : Setting.gantt_items_limit.to_i
|
83
|
end
|
84
|
end
|
85
|
|
86
|
def common_params
|
87
|
{ :controller => 'gantts', :action => 'show', :project_id => @project }
|
88
|
end
|
89
|
|
90
|
def params
|
91
|
common_params.merge({ :zoom => zoom, :year => year_from, :month => month_from, :months => months })
|
92
|
end
|
93
|
|
94
|
def params_previous
|
95
|
common_params.merge({:year => (date_from << months).year, :month => (date_from << months).month, :zoom => zoom, :months => months })
|
96
|
end
|
97
|
|
98
|
def params_next
|
99
|
common_params.merge({:year => (date_from >> months).year, :month => (date_from >> months).month, :zoom => zoom, :months => months })
|
100
|
end
|
101
|
|
102
|
### Extracted from the HTML view/helpers
|
103
|
# Returns the number of rows that will be rendered on the Gantt chart
|
104
|
def number_of_rows
|
105
|
return @number_of_rows if @number_of_rows
|
106
|
|
107
|
rows = if @project
|
108
|
number_of_rows_on_project(@project)
|
109
|
else
|
110
|
Project.roots.visible.has_module('issue_tracking').inject(0) do |total, project|
|
111
|
total += number_of_rows_on_project(project)
|
112
|
end
|
113
|
end
|
114
|
|
115
|
rows > @max_rows ? @max_rows : rows
|
116
|
end
|
117
|
|
118
|
# Returns the number of rows that will be used to list a project on
|
119
|
# the Gantt chart. This will recurse for each subproject.
|
120
|
def number_of_rows_on_project(project)
|
121
|
# Remove the project requirement for Versions because it will
|
122
|
# restrict issues to only be on the current project. This
|
123
|
# ends up missing issues which are assigned to shared versions.
|
124
|
@query.project = nil if @query.project
|
125
|
|
126
|
# One Root project
|
127
|
count = 1
|
128
|
# Issues without a Version
|
129
|
count += project.issues.for_gantt.without_version.with_query(@query).count
|
130
|
|
131
|
# Versions
|
132
|
count += project.versions.count
|
133
|
|
134
|
# Issues on the Versions
|
135
|
project.versions.each do |version|
|
136
|
count += version.fixed_issues.for_gantt.with_query(@query).count
|
137
|
end
|
138
|
|
139
|
# Subprojects
|
140
|
project.children.visible.has_module('issue_tracking').each do |subproject|
|
141
|
count += number_of_rows_on_project(subproject)
|
142
|
end
|
143
|
|
144
|
count
|
145
|
end
|
146
|
|
147
|
# Renders the subjects of the Gantt chart, the left side.
|
148
|
def subjects(options={})
|
149
|
render(options.merge(:only => :subjects)) unless @subjects_rendered
|
150
|
@subjects
|
151
|
end
|
152
|
|
153
|
# Renders the lines of the Gantt chart, the right side
|
154
|
def lines(options={})
|
155
|
render(options.merge(:only => :lines)) unless @lines_rendered
|
156
|
@lines
|
157
|
end
|
158
|
|
159
|
# Renders the calendars of the Gantt chart, the right side
|
160
|
def calendars(options={})
|
161
|
render(options.merge(:only => :calendars)) unless @calendars_rendered
|
162
|
@calendars
|
163
|
end
|
164
|
|
165
|
def render(options={})
|
166
|
options = {:indent => 4, :render => :subject, :format => :html}.merge(options)
|
167
|
|
168
|
if options[:format] == :html
|
169
|
@subjects = '' unless options[:only] == :lines && options[:only] == :calendars
|
170
|
@lines = '' unless options[:only] == :subjects && options[:only] == :calendars
|
171
|
@calendars = '' unless options[:only] == :lines && options[:only] == :subjects
|
172
|
else
|
173
|
@subjects = '' unless options[:only] == :lines
|
174
|
@lines = '' unless options[:only] == :subjects
|
175
|
end
|
176
|
@number_of_rows = 0
|
177
|
|
178
|
if @project
|
179
|
render_project(@project, options)
|
180
|
else
|
181
|
Project.roots.visible.has_module('issue_tracking').each do |project|
|
182
|
render_project(project, options)
|
183
|
break if abort?
|
184
|
end
|
185
|
end
|
186
|
|
187
|
if options[:format] == :html
|
188
|
@subjects_rendered = true unless options[:only] == :lines && options[:only] == :calendars
|
189
|
@lines_rendered = true unless options[:only] == :subjects && options[:only] == :calendars
|
190
|
@calendars_rendered = true unless options[:only] == :lines && options[:only] == :subjects
|
191
|
else
|
192
|
@subjects_rendered = true unless options[:only] == :lines
|
193
|
@lines_rendered = true unless options[:only] == :subjects
|
194
|
end
|
195
|
|
196
|
render_end(options)
|
197
|
end
|
198
|
|
199
|
def render_project(project, options={})
|
200
|
options[:top] = 0 unless options.key? :top
|
201
|
options[:indent_increment] = 20 unless options.key? :indent_increment
|
202
|
options[:top_increment] = 18 unless options.key? :top_increment
|
203
|
|
204
|
if options[:format] == :html
|
205
|
subject_for_project(project, options) unless options[:only] == :lines && options[:only] == :calendars
|
206
|
line_for_project(project, options) unless options[:only] == :subjects && options[:only] == :calendars
|
207
|
calendar_for_project(project, options) unless options[:only] == :lines && options[:only] == :subjects
|
208
|
else
|
209
|
subject_for_project(project, options) unless options[:only] == :lines
|
210
|
line_for_project(project, options) unless options[:only] == :subjects
|
211
|
end
|
212
|
|
213
|
options[:top] += options[:top_increment]
|
214
|
options[:indent] += options[:indent_increment]
|
215
|
@number_of_rows += 1
|
216
|
return if abort?
|
217
|
|
218
|
# Second, Issues without a version
|
219
|
issues = project.issues.for_gantt.without_version.with_query(@query).all(:limit => current_limit)
|
220
|
sort_issues!(issues)
|
221
|
if issues
|
222
|
render_issues(issues, options)
|
223
|
return if abort?
|
224
|
end
|
225
|
|
226
|
# Third, Versions
|
227
|
project.versions.sort.each do |version|
|
228
|
render_version(version, options)
|
229
|
return if abort?
|
230
|
end
|
231
|
|
232
|
# Fourth, subprojects
|
233
|
project.children.visible.has_module('issue_tracking').each do |project|
|
234
|
render_project(project, options)
|
235
|
return if abort?
|
236
|
end unless project.leaf?
|
237
|
|
238
|
# Remove indent to hit the next sibling
|
239
|
options[:indent] -= options[:indent_increment]
|
240
|
end
|
241
|
|
242
|
def render_issues(issues, options={})
|
243
|
@issue_ancestors = []
|
244
|
|
245
|
issues.each do |i|
|
246
|
if options[:format] == :html
|
247
|
subject_for_issue(i, options) unless options[:only] == :lines && options[:only] == :calendars
|
248
|
line_for_issue(i, options) unless options[:only] == :subjects && options[:only] == :calendars
|
249
|
calendar_for_issue(i, options) unless options[:only] == :lines && options[:only] == :subjects
|
250
|
else
|
251
|
subject_for_issue(i, options) unless options[:only] == :lines
|
252
|
line_for_issue(i, options) unless options[:only] == :subjects
|
253
|
end
|
254
|
|
255
|
options[:top] += options[:top_increment]
|
256
|
@number_of_rows += 1
|
257
|
break if abort?
|
258
|
end
|
259
|
|
260
|
options[:indent] -= (options[:indent_increment] * @issue_ancestors.size)
|
261
|
end
|
262
|
|
263
|
def render_version(version, options={})
|
264
|
# Version header
|
265
|
if options[:format] == :html
|
266
|
subject_for_version(version, options) unless options[:only] == :lines && options[:only] == :calendars
|
267
|
line_for_version(version, options) unless options[:only] == :subjects && options[:only] == :calendars
|
268
|
calendar_for_version(version, options) unless options[:only] == :lines && options[:only] == :subjects
|
269
|
else
|
270
|
subject_for_version(version, options) unless options[:only] == :lines
|
271
|
line_for_version(version, options) unless options[:only] == :subjects
|
272
|
end
|
273
|
|
274
|
options[:top] += options[:top_increment]
|
275
|
@number_of_rows += 1
|
276
|
return if abort?
|
277
|
|
278
|
# Remove the project requirement for Versions because it will
|
279
|
# restrict issues to only be on the current project. This
|
280
|
# ends up missing issues which are assigned to shared versions.
|
281
|
@query.project = nil if @query.project
|
282
|
|
283
|
issues = version.fixed_issues.for_gantt.with_query(@query).all(:limit => current_limit)
|
284
|
if issues
|
285
|
sort_issues!(issues)
|
286
|
# Indent issues
|
287
|
options[:indent] += options[:indent_increment]
|
288
|
render_issues(issues, options)
|
289
|
options[:indent] -= options[:indent_increment]
|
290
|
end
|
291
|
end
|
292
|
|
293
|
def render_end(options={})
|
294
|
case options[:format]
|
295
|
when :pdf
|
296
|
options[:pdf].Line(15, options[:top], PDF::TotalWidth, options[:top])
|
297
|
end
|
298
|
end
|
299
|
|
300
|
def subject_for_project(project, options)
|
301
|
case options[:format]
|
302
|
when :html
|
303
|
subject = "<span class='icon icon-projects #{project.overdue? ? 'project-overdue' : ''}'>"
|
304
|
subject << view.link_to_project(project)
|
305
|
subject << '</span>'
|
306
|
html_subject(options, subject, :css => "project-name")
|
307
|
when :image
|
308
|
image_subject(options, project.name)
|
309
|
when :pdf
|
310
|
pdf_new_page?(options)
|
311
|
pdf_subject(options, project.name)
|
312
|
end
|
313
|
end
|
314
|
|
315
|
def line_for_project(project, options)
|
316
|
# Skip versions that don't have a start_date or due date
|
317
|
if project.is_a?(Project) && project.start_date && project.due_date
|
318
|
options[:zoom] ||= 1
|
319
|
options[:g_width] ||= (self.date_to - self.date_from + 1) * options[:zoom]
|
320
|
|
321
|
coords = coordinates(project.start_date, project.due_date, nil, options[:zoom])
|
322
|
label = h(project)
|
323
|
|
324
|
case options[:format]
|
325
|
when :html
|
326
|
html_task(options, coords, :css => "project task", :label => label, :markers => true, :id => project.id, :kind => "p")
|
327
|
when :image
|
328
|
image_task(options, coords, :label => label, :markers => true, :height => 3)
|
329
|
when :pdf
|
330
|
pdf_task(options, coords, :label => label, :markers => true, :height => 0.8)
|
331
|
end
|
332
|
else
|
333
|
ActiveRecord::Base.logger.debug "Gantt#line_for_project was not given a project with a start_date"
|
334
|
''
|
335
|
end
|
336
|
end
|
337
|
|
338
|
def subject_for_version(version, options)
|
339
|
case options[:format]
|
340
|
when :html
|
341
|
subject = "<span class='icon icon-package #{version.behind_schedule? ? 'version-behind-schedule' : ''} #{version.overdue? ? 'version-overdue' : ''}'>"
|
342
|
subject << view.link_to_version(version)
|
343
|
subject << '</span>'
|
344
|
html_subject(options, subject, :css => "version-name")
|
345
|
when :image
|
346
|
image_subject(options, version.to_s_with_project)
|
347
|
when :pdf
|
348
|
pdf_new_page?(options)
|
349
|
pdf_subject(options, version.to_s_with_project)
|
350
|
end
|
351
|
end
|
352
|
|
353
|
def line_for_version(version, options)
|
354
|
# Skip versions that don't have a start_date
|
355
|
if version.is_a?(Version) && version.start_date && version.due_date
|
356
|
options[:zoom] ||= 1
|
357
|
options[:g_width] ||= (self.date_to - self.date_from + 1) * options[:zoom]
|
358
|
|
359
|
coords = coordinates(version.start_date, version.due_date, version.completed_pourcent, options[:zoom])
|
360
|
label = "#{h version } #{h version.completed_pourcent.to_i.to_s}%"
|
361
|
label = h("#{version.project} -") + label unless @project && @project == version.project
|
362
|
|
363
|
case options[:format]
|
364
|
when :html
|
365
|
html_task(options, coords, :css => "version task", :label => label, :markers => true, :id => version.id, :kind => "v")
|
366
|
when :image
|
367
|
image_task(options, coords, :label => label, :markers => true, :height => 3)
|
368
|
when :pdf
|
369
|
pdf_task(options, coords, :label => label, :markers => true, :height => 0.8)
|
370
|
end
|
371
|
else
|
372
|
ActiveRecord::Base.logger.debug "Gantt#line_for_version was not given a version with a start_date"
|
373
|
''
|
374
|
end
|
375
|
end
|
376
|
|
377
|
def subject_for_issue(issue, options)
|
378
|
while @issue_ancestors.any? && !issue.is_descendant_of?(@issue_ancestors.last)
|
379
|
@issue_ancestors.pop
|
380
|
options[:indent] -= options[:indent_increment]
|
381
|
end
|
382
|
|
383
|
output = case options[:format]
|
384
|
when :html
|
385
|
css_classes = ''
|
386
|
css_classes << ' issue-overdue' if issue.overdue?
|
387
|
css_classes << ' issue-behind-schedule' if issue.behind_schedule?
|
388
|
css_classes << ' icon icon-issue' unless Setting.gravatar_enabled? && issue.assigned_to
|
389
|
|
390
|
subject = "<span class='#{css_classes}'>"
|
391
|
if issue.assigned_to.present?
|
392
|
assigned_string = l(:field_assigned_to) + ": " + issue.assigned_to.name
|
393
|
subject << view.avatar(issue.assigned_to, :class => 'gravatar icon-gravatar', :size => 10, :title => assigned_string).to_s
|
394
|
end
|
395
|
subject << view.link_to_issue(issue)
|
396
|
subject << '</span>'
|
397
|
html_subject(options, subject, :css => "issue-subject", :title => issue.subject) + "\n"
|
398
|
when :image
|
399
|
image_subject(options, issue.subject)
|
400
|
when :pdf
|
401
|
pdf_new_page?(options)
|
402
|
pdf_subject(options, issue.subject)
|
403
|
end
|
404
|
|
405
|
unless issue.leaf?
|
406
|
@issue_ancestors << issue
|
407
|
options[:indent] += options[:indent_increment]
|
408
|
end
|
409
|
|
410
|
output
|
411
|
end
|
412
|
|
413
|
def line_for_issue(issue, options)
|
414
|
# Skip issues that don't have a due_before (due_date or version's due_date)
|
415
|
if issue.is_a?(Issue) && issue.due_before
|
416
|
coords = coordinates(issue.start_date, issue.due_before, issue.done_ratio, options[:zoom])
|
417
|
label = "#{ issue.status.name } #{ issue.done_ratio }%"
|
418
|
if !issue.due_date && issue.fixed_version
|
419
|
if options[:format] == :html
|
420
|
label += "- <strong>#{h(issue.fixed_version.name)}</strong>"
|
421
|
else
|
422
|
label += "-#{h(issue.fixed_version.name)}"
|
423
|
end
|
424
|
end
|
425
|
|
426
|
case options[:format]
|
427
|
when :html
|
428
|
html_task(options, coords, :css => "task " + (issue.leaf? ? 'leaf' : 'parent'), :label => label, :issue => issue, :markers => !issue.leaf?, :id => issue.id, :kind => "i")
|
429
|
when :image
|
430
|
image_task(options, coords, :label => label)
|
431
|
when :pdf
|
432
|
pdf_task(options, coords, :label => label)
|
433
|
end
|
434
|
else
|
435
|
ActiveRecord::Base.logger.debug "GanttHelper#line_for_issue was not given an issue with a due_before"
|
436
|
''
|
437
|
end
|
438
|
end
|
439
|
|
440
|
# Generates a gantt image
|
441
|
# Only defined if RMagick is avalaible
|
442
|
def to_image(format='PNG')
|
443
|
date_to = (@date_from >> @months)-1
|
444
|
show_weeks = @zoom > 1
|
445
|
show_days = @zoom > 2
|
446
|
|
447
|
subject_width = 400
|
448
|
header_heigth = 18
|
449
|
# width of one day in pixels
|
450
|
zoom = @zoom*2
|
451
|
g_width = (@date_to - @date_from + 1)*zoom
|
452
|
g_height = 20 * number_of_rows + 30
|
453
|
headers_heigth = (show_weeks ? 2*header_heigth : header_heigth)
|
454
|
height = g_height + headers_heigth
|
455
|
|
456
|
imgl = Magick::ImageList.new
|
457
|
imgl.new_image(subject_width+g_width+1, height)
|
458
|
gc = Magick::Draw.new
|
459
|
|
460
|
gc.font = "C:\\WINDOWS\\FONTS\\MSGOTHIC.TTC" # add 2011/01/14: m.shibata for Japanese
|
461
|
gc.pointsize = 12 # add 2011/01/14: m.shibata for Japanese
|
462
|
|
463
|
# Subjects
|
464
|
gc.stroke('transparent')
|
465
|
subjects(:image => gc, :top => (headers_heigth + 20), :indent => 4, :format => :image)
|
466
|
|
467
|
# Months headers
|
468
|
month_f = @date_from
|
469
|
left = subject_width
|
470
|
@months.times do
|
471
|
width = ((month_f >> 1) - month_f) * zoom
|
472
|
gc.fill('white')
|
473
|
gc.stroke('grey')
|
474
|
gc.stroke_width(1)
|
475
|
gc.rectangle(left, 0, left + width, height)
|
476
|
gc.fill('black')
|
477
|
gc.stroke('transparent')
|
478
|
gc.stroke_width(1)
|
479
|
gc.text(left.round + 8, 14, "#{month_f.year}-#{month_f.month}")
|
480
|
left = left + width
|
481
|
month_f = month_f >> 1
|
482
|
end
|
483
|
|
484
|
# Weeks headers
|
485
|
if show_weeks
|
486
|
left = subject_width
|
487
|
height = header_heigth
|
488
|
if @date_from.cwday == 1
|
489
|
# date_from is monday
|
490
|
week_f = date_from
|
491
|
else
|
492
|
# find next monday after date_from
|
493
|
week_f = @date_from + (7 - @date_from.cwday + 1)
|
494
|
width = (7 - @date_from.cwday + 1) * zoom
|
495
|
gc.fill('white')
|
496
|
gc.stroke('grey')
|
497
|
gc.stroke_width(1)
|
498
|
gc.rectangle(left, header_heigth, left + width, 2*header_heigth + g_height-1)
|
499
|
left = left + width
|
500
|
end
|
501
|
while week_f <= date_to
|
502
|
width = (week_f + 6 <= date_to) ? 7 * zoom : (date_to - week_f + 1) * zoom
|
503
|
gc.fill('white')
|
504
|
gc.stroke('grey')
|
505
|
gc.stroke_width(1)
|
506
|
gc.rectangle(left.round, header_heigth, left.round + width, 2*header_heigth + g_height-1)
|
507
|
gc.fill('black')
|
508
|
gc.stroke('transparent')
|
509
|
gc.stroke_width(1)
|
510
|
gc.text(left.round + 2, header_heigth + 14, week_f.cweek.to_s)
|
511
|
left = left + width
|
512
|
week_f = week_f+7
|
513
|
end
|
514
|
end
|
515
|
|
516
|
# Days details (week-end in grey)
|
517
|
if show_days
|
518
|
left = subject_width
|
519
|
height = g_height + header_heigth - 1
|
520
|
wday = @date_from.cwday
|
521
|
(date_to - @date_from + 1).to_i.times do
|
522
|
width = zoom
|
523
|
gc.fill(wday == 6 || wday == 7 ? '#eee' : 'white')
|
524
|
gc.stroke('#ddd')
|
525
|
gc.stroke_width(1)
|
526
|
gc.rectangle(left, 2*header_heigth, left + width, 2*header_heigth + g_height-1)
|
527
|
left = left + width
|
528
|
wday = wday + 1
|
529
|
wday = 1 if wday > 7
|
530
|
end
|
531
|
end
|
532
|
|
533
|
# border
|
534
|
gc.fill('transparent')
|
535
|
gc.stroke('grey')
|
536
|
gc.stroke_width(1)
|
537
|
gc.rectangle(0, 0, subject_width+g_width, headers_heigth)
|
538
|
gc.stroke('black')
|
539
|
gc.rectangle(0, 0, subject_width+g_width, g_height+ headers_heigth-1)
|
540
|
|
541
|
# content
|
542
|
top = headers_heigth + 20
|
543
|
|
544
|
gc.stroke('transparent')
|
545
|
lines(:image => gc, :top => top, :zoom => zoom, :subject_width => subject_width, :format => :image)
|
546
|
|
547
|
# today red line
|
548
|
if Date.today >= @date_from and Date.today <= date_to
|
549
|
gc.stroke('red')
|
550
|
x = (Date.today-@date_from+1)*zoom + subject_width
|
551
|
gc.line(x, headers_heigth, x, headers_heigth + g_height-1)
|
552
|
end
|
553
|
|
554
|
gc.draw(imgl)
|
555
|
imgl.format = format
|
556
|
imgl.to_blob
|
557
|
end if Object.const_defined?(:Magick)
|
558
|
|
559
|
def to_pdf
|
560
|
pdf = ::Redmine::Export::PDF::IFPDF.new(current_language)
|
561
|
pdf.SetTitle("#{l(:label_gantt)} #{project}")
|
562
|
pdf.AliasNbPages
|
563
|
pdf.footer_date = format_date(Date.today)
|
564
|
pdf.AddPage("L")
|
565
|
pdf.SetFontStyle('B',12)
|
566
|
pdf.SetX(15)
|
567
|
pdf.Cell(PDF::LeftPaneWidth, 20, project.to_s)
|
568
|
pdf.Ln
|
569
|
pdf.SetFontStyle('B',9)
|
570
|
|
571
|
subject_width = PDF::LeftPaneWidth
|
572
|
header_heigth = 5
|
573
|
|
574
|
headers_heigth = header_heigth
|
575
|
show_weeks = false
|
576
|
show_days = false
|
577
|
|
578
|
if self.months < 7
|
579
|
show_weeks = true
|
580
|
headers_heigth = 2*header_heigth
|
581
|
if self.months < 3
|
582
|
show_days = true
|
583
|
headers_heigth = 3*header_heigth
|
584
|
end
|
585
|
end
|
586
|
|
587
|
g_width = PDF.right_pane_width
|
588
|
zoom = (g_width) / (self.date_to - self.date_from + 1)
|
589
|
g_height = 120
|
590
|
t_height = g_height + headers_heigth
|
591
|
|
592
|
y_start = pdf.GetY
|
593
|
|
594
|
# Months headers
|
595
|
month_f = self.date_from
|
596
|
left = subject_width
|
597
|
height = header_heigth
|
598
|
self.months.times do
|
599
|
width = ((month_f >> 1) - month_f) * zoom
|
600
|
pdf.SetY(y_start)
|
601
|
pdf.SetX(left)
|
602
|
pdf.Cell(width, height, "#{month_f.year}-#{month_f.month}", "LTR", 0, "C")
|
603
|
left = left + width
|
604
|
month_f = month_f >> 1
|
605
|
end
|
606
|
|
607
|
# Weeks headers
|
608
|
if show_weeks
|
609
|
left = subject_width
|
610
|
height = header_heigth
|
611
|
if self.date_from.cwday == 1
|
612
|
# self.date_from is monday
|
613
|
week_f = self.date_from
|
614
|
else
|
615
|
# find next monday after self.date_from
|
616
|
week_f = self.date_from + (7 - self.date_from.cwday + 1)
|
617
|
width = (7 - self.date_from.cwday + 1) * zoom-1
|
618
|
pdf.SetY(y_start + header_heigth)
|
619
|
pdf.SetX(left)
|
620
|
pdf.Cell(width + 1, height, "", "LTR")
|
621
|
left = left + width+1
|
622
|
end
|
623
|
while week_f <= self.date_to
|
624
|
width = (week_f + 6 <= self.date_to) ? 7 * zoom : (self.date_to - week_f + 1) * zoom
|
625
|
pdf.SetY(y_start + header_heigth)
|
626
|
pdf.SetX(left)
|
627
|
pdf.Cell(width, height, (width >= 5 ? week_f.cweek.to_s : ""), "LTR", 0, "C")
|
628
|
left = left + width
|
629
|
week_f = week_f+7
|
630
|
end
|
631
|
end
|
632
|
|
633
|
# Days headers
|
634
|
if show_days
|
635
|
left = subject_width
|
636
|
height = header_heigth
|
637
|
wday = self.date_from.cwday
|
638
|
pdf.SetFontStyle('B',7)
|
639
|
(self.date_to - self.date_from + 1).to_i.times do
|
640
|
width = zoom
|
641
|
pdf.SetY(y_start + 2 * header_heigth)
|
642
|
pdf.SetX(left)
|
643
|
pdf.Cell(width, height, day_name(wday).first, "LTR", 0, "C")
|
644
|
left = left + width
|
645
|
wday = wday + 1
|
646
|
wday = 1 if wday > 7
|
647
|
end
|
648
|
end
|
649
|
|
650
|
pdf.SetY(y_start)
|
651
|
pdf.SetX(15)
|
652
|
pdf.Cell(subject_width+g_width-15, headers_heigth, "", 1)
|
653
|
|
654
|
# Tasks
|
655
|
top = headers_heigth + y_start
|
656
|
options = {
|
657
|
:top => top,
|
658
|
:zoom => zoom,
|
659
|
:subject_width => subject_width,
|
660
|
:g_width => g_width,
|
661
|
:indent => 0,
|
662
|
:indent_increment => 5,
|
663
|
:top_increment => 5,
|
664
|
:format => :pdf,
|
665
|
:pdf => pdf
|
666
|
}
|
667
|
render(options)
|
668
|
pdf.Output
|
669
|
end
|
670
|
|
671
|
def edit(pms)
|
672
|
id = pms[:id]
|
673
|
kind = id.slice!(0).chr
|
674
|
begin
|
675
|
case kind
|
676
|
when 'i'
|
677
|
@issue = Issue.find(pms[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
|
678
|
when 'p'
|
679
|
@issue = Project.find(pms[:id])
|
680
|
when 'v'
|
681
|
@issue = Version.find(pms[:id], :include => [:project])
|
682
|
end
|
683
|
rescue ActiveRecord::RecordNotFound
|
684
|
return "issue not found : #{pms[:id]}", 400
|
685
|
end
|
686
|
|
687
|
if !@issue.start_date || !@issue.due_before
|
688
|
#render :text=>l(:notice_locking_conflict), :status=>400
|
689
|
return l(:notice_locking_conflict), 400
|
690
|
end
|
691
|
@issue.init_journal(User.current)
|
692
|
date_from = Date.parse(pms[:date_from])
|
693
|
old_start_date = @issue.start_date
|
694
|
o = get_issue_position(@issue, pms[:zoom])
|
695
|
text_for_revert = "#{kind}#{id}=#{format_date(@issue.start_date)},#{@issue.start_date},#{format_date(@issue.due_before)},#{@issue.due_before},#{o[0]},#{o[1]},#{o[2]},#{o[3]}"
|
696
|
|
697
|
if pms[:day]
|
698
|
#bar moved
|
699
|
day = pms[:day].to_i
|
700
|
duration = @issue.due_before - @issue.start_date
|
701
|
@issue.start_date = date_from + day
|
702
|
@issue.due_date = @issue.start_date + duration.to_i if @issue.due_date
|
703
|
elsif pms[:start_date]
|
704
|
#start date changed
|
705
|
start_date = Date.parse(pms[:start_date])
|
706
|
if @issue.start_date == start_date
|
707
|
#render :text=>""
|
708
|
return "", 200 #nothing has changed
|
709
|
end
|
710
|
@issue.start_date = start_date
|
711
|
@issue.due_date = start_date if @issue.due_date && start_date > @issue.due_date
|
712
|
elsif pms[:due_date]
|
713
|
#due date changed
|
714
|
due_date = Date.parse(pms[:due_date])
|
715
|
if @issue.due_date == due_date
|
716
|
#render :text=>""
|
717
|
return "", 200 #nothing has changed
|
718
|
end
|
719
|
@issue.due_date = due_date
|
720
|
@issue.start_date = due_date if due_date < @issue.start_date
|
721
|
end
|
722
|
fv = @issue.fixed_version
|
723
|
if fv && fv.effective_date && !@issue.due_date && fv.effective_date < @issue.start_date
|
724
|
@issue.start_date = old_start_date
|
725
|
end
|
726
|
|
727
|
begin
|
728
|
@issue.save!
|
729
|
o = get_issue_position(@issue, pms[:zoom])
|
730
|
text = "#{kind}#{id}=#{format_date(@issue.start_date)},#{@issue.start_date},#{format_date(@issue.due_before)},#{@issue.due_before},#{o[0]},#{o[1]},#{o[2]},#{o[3]}"
|
731
|
|
732
|
prj_map = {}
|
733
|
text = set_project_data(@issue.project, pms[:zoom], text, prj_map)
|
734
|
version_map = {}
|
735
|
text = set_version_data(@issue.fixed_version, pms[:zoom], text, version_map)
|
736
|
|
737
|
#check dependencies
|
738
|
issues = @issue.all_precedes_issues
|
739
|
issues.each do |i|
|
740
|
o = get_issue_position(i, pms[:zoom])
|
741
|
text += "|i#{i.id}=#{format_date(i.start_date)},#{i.start_date},#{format_date(i.due_before)},#{i.due_before},#{o[0]},#{o[1]},#{o[2]},#{o[3]}"
|
742
|
text = set_project_data(i.project, pms[:zoom], text, prj_map)
|
743
|
text = set_version_data(i.fixed_version, pms[:zoom], text, version_map)
|
744
|
end
|
745
|
#render :text=>text
|
746
|
return text, 200
|
747
|
rescue => e
|
748
|
#render :text=>@issue.errors.full_messages.join("\n") + "|" + text_for_revert , :status=>400
|
749
|
if @issue.errors.full_messages.to_s == ""
|
750
|
return e.to_s + "\n" + [$!,$@.join("\n")].join("\n") + "\n" + @issue.errors.full_messages.join("\n") + "|" + text_for_revert, 400
|
751
|
else
|
752
|
return @issue.errors.full_messages.join("\n") + "|" + text_for_revert, 400
|
753
|
end
|
754
|
end
|
755
|
end
|
756
|
|
757
|
private
|
758
|
|
759
|
def coordinates(start_date, end_date, progress, zoom=nil)
|
760
|
zoom ||= @zoom
|
761
|
|
762
|
coords = {}
|
763
|
if start_date && end_date && start_date < self.date_to && end_date > self.date_from
|
764
|
if start_date > self.date_from
|
765
|
coords[:start] = start_date - self.date_from
|
766
|
coords[:bar_start] = start_date - self.date_from
|
767
|
else
|
768
|
coords[:bar_start] = 0
|
769
|
end
|
770
|
if end_date < self.date_to
|
771
|
coords[:end] = end_date - self.date_from
|
772
|
coords[:bar_end] = end_date - self.date_from + 1
|
773
|
else
|
774
|
coords[:bar_end] = self.date_to - self.date_from + 1
|
775
|
end
|
776
|
|
777
|
if progress
|
778
|
progress_date = start_date + (end_date - start_date) * (progress / 100.0)
|
779
|
if progress_date > self.date_from && progress_date > start_date
|
780
|
if progress_date < self.date_to
|
781
|
coords[:bar_progress_end] = progress_date - self.date_from + 1
|
782
|
else
|
783
|
coords[:bar_progress_end] = self.date_to - self.date_from + 1
|
784
|
end
|
785
|
end
|
786
|
|
787
|
if progress_date < Date.today
|
788
|
late_date = [Date.today, end_date].min
|
789
|
if late_date > self.date_from && late_date > start_date
|
790
|
if late_date < self.date_to
|
791
|
coords[:bar_late_end] = late_date - self.date_from + 1
|
792
|
else
|
793
|
coords[:bar_late_end] = self.date_to - self.date_from + 1
|
794
|
end
|
795
|
end
|
796
|
end
|
797
|
end
|
798
|
end
|
799
|
|
800
|
# Transforms dates into pixels witdh
|
801
|
coords.keys.each do |key|
|
802
|
coords[key] = (coords[key] * zoom).floor
|
803
|
end
|
804
|
coords
|
805
|
end
|
806
|
|
807
|
# Sorts a collection of issues by start_date, due_date, id for gantt rendering
|
808
|
def sort_issues!(issues)
|
809
|
issues.sort! { |a, b| gantt_issue_compare(a, b, issues) }
|
810
|
end
|
811
|
|
812
|
# TODO: top level issues should be sorted by start date
|
813
|
def gantt_issue_compare(x, y, issues)
|
814
|
if x.root_id == y.root_id
|
815
|
x.lft <=> y.lft
|
816
|
else
|
817
|
x.root_id <=> y.root_id
|
818
|
end
|
819
|
end
|
820
|
|
821
|
def current_limit
|
822
|
if @max_rows
|
823
|
@max_rows - @number_of_rows
|
824
|
else
|
825
|
nil
|
826
|
end
|
827
|
end
|
828
|
|
829
|
def abort?
|
830
|
if @max_rows && @number_of_rows >= @max_rows
|
831
|
@truncated = true
|
832
|
end
|
833
|
end
|
834
|
|
835
|
def pdf_new_page?(options)
|
836
|
if options[:top] > 180
|
837
|
options[:pdf].Line(15, options[:top], PDF::TotalWidth, options[:top])
|
838
|
options[:pdf].AddPage("L")
|
839
|
options[:top] = 15
|
840
|
options[:pdf].Line(15, options[:top] - 0.1, PDF::TotalWidth, options[:top] - 0.1)
|
841
|
end
|
842
|
end
|
843
|
|
844
|
def html_subject(params, subject, options={})
|
845
|
style = "position: absolute;top:#{params[:top]}px;left:#{params[:indent]}px;"
|
846
|
style << "width:#{params[:subject_width] - params[:indent]}px;" if params[:subject_width]
|
847
|
|
848
|
output = view.content_tag 'div', subject, :class => options[:css], :style => style, :title => options[:title]
|
849
|
@subjects << output
|
850
|
output
|
851
|
end
|
852
|
|
853
|
def pdf_subject(params, subject, options={})
|
854
|
params[:pdf].SetY(params[:top])
|
855
|
params[:pdf].SetX(15)
|
856
|
|
857
|
char_limit = PDF::MaxCharactorsForSubject - params[:indent]
|
858
|
params[:pdf].Cell(params[:subject_width]-15, 5, (" " * params[:indent]) + subject.to_s.sub(/^(.{#{char_limit}}[^\s]*\s).*$/, '\1 (...)'), "LR")
|
859
|
|
860
|
params[:pdf].SetY(params[:top])
|
861
|
params[:pdf].SetX(params[:subject_width])
|
862
|
params[:pdf].Cell(params[:g_width], 5, "", "LR")
|
863
|
end
|
864
|
|
865
|
def image_subject(params, subject, options={})
|
866
|
params[:image].fill('black')
|
867
|
params[:image].stroke('transparent')
|
868
|
params[:image].stroke_width(1)
|
869
|
params[:image].text(params[:indent], params[:top] + 2, subject)
|
870
|
end
|
871
|
|
872
|
def html_task(params, coords, options={})
|
873
|
output = ''
|
874
|
# Renders the task bar, with progress and late
|
875
|
if coords[:bar_start] && coords[:bar_end]
|
876
|
i_width = coords[:bar_end] - coords[:bar_start] - 2
|
877
|
output << "<div id='ev_#{options[:kind]}#{options[:id]}' style='position:absolute;left:#{coords[:bar_start]}px;top:#{params[:top]}px;padding-top:3px;height:18px;width:#{ i_width + 100}px;' #{options[:kind] == 'i' ? "class='handle'" : ""}>"
|
878
|
output << "<div id='task_todo_#{options[:kind]}#{options[:id]}' style='float:left:0px; width:#{ i_width}px;' class='#{options[:css]} task_todo'> </div>"
|
879
|
|
880
|
if coords[:bar_late_end]
|
881
|
l_width = coords[:bar_late_end] - coords[:bar_start] - 2
|
882
|
output << "<div id='task_late_#{options[:kind]}#{options[:id]}' style='float:left:0px; width:#{ l_width}px;' class='#{ l_width == 0 ? options[:css] + " task_none" : options[:css] + " task_late"}'> </div>"
|
883
|
else
|
884
|
output << "<div id='task_late_#{options[:kind]}#{options[:id]}' style='float:left:0px; width:0px;' class='#{ options[:css] + " task_none"}'> </div>"
|
885
|
end
|
886
|
if coords[:bar_progress_end]
|
887
|
d_width = coords[:bar_progress_end] - coords[:bar_start] - 2
|
888
|
output << "<div id='task_done_#{options[:kind]}#{options[:id]}' style='float:left:0px; width:#{ d_width}px;' class='#{ d_width == 0 ? options[:css] + " task_none" : options[:css] + " task_done"}'> </div>"
|
889
|
else
|
890
|
output << "<div id='task_done_#{options[:kind]}#{options[:id]}' style='float:left:0px; width:0px;' class='#{ options[:css] + " task_none"}'> </div>"
|
891
|
end
|
892
|
output << "</div>"
|
893
|
else
|
894
|
output << "<div id='ev_#{options[:kind]}#{options[:id]}' style='position:absolute;left:0px;top:#{params[:top]}px;padding-top:3px;height:18px;width:0px;' #{options[:kind] == 'i' ? "class='handle'" : ""}>"
|
895
|
output << "<div id='task_todo_#{options[:kind]}#{options[:id]}' style='float:left:0px; width:0px;' class='#{ options[:css]} task_todo'> </div>"
|
896
|
output << "<div id='task_late_#{options[:kind]}#{options[:id]}' style='float:left:0px; width:0px;' class='#{ options[:css] + " task_none"}'> </div>"
|
897
|
output << "<div id='task_done_#{options[:kind]}#{options[:id]}' style='float:left:0px; width:0px;' class='#{ options[:css] + " task_none"}'> </div>"
|
898
|
output << "</div>"
|
899
|
end
|
900
|
# Renders the markers
|
901
|
if options[:markers]
|
902
|
if coords[:start]
|
903
|
output << "<div id='marker_start_#{options[:kind]}#{options[:id]}' style='top:#{ params[:top] }px;left:#{ coords[:start] }px;width:15px;' class='#{options[:css]} marker starting'> </div>"
|
904
|
end
|
905
|
if coords[:end]
|
906
|
output << "<div id='marker_end_#{options[:kind]}#{options[:id]}' style='top:#{ params[:top] }px;left:#{ coords[:end] + params[:zoom] }px;width:15px;' class='#{options[:css]} marker ending'> </div>"
|
907
|
end
|
908
|
end
|
909
|
# Renders the label on the right
|
910
|
if options[:label]
|
911
|
output << "<div id='label_#{options[:kind]}#{options[:id]}' style='top:#{ params[:top] }px;left:#{ (coords[:bar_end] || 0) + 8 }px;' class='#{options[:css]} label'>"
|
912
|
output << options[:label]
|
913
|
output << "</div>"
|
914
|
end
|
915
|
# Renders the tooltip
|
916
|
if options[:issue] && coords[:bar_start] && coords[:bar_end]
|
917
|
output << "<div id='tt_#{options[:kind]}#{options[:id]}' class='tooltip' style='position: absolute;top:#{ params[:top] }px;left:#{ coords[:bar_start] }px;width:#{ coords[:bar_end] - coords[:bar_start] }px;height:12px;'>"
|
918
|
output << '<span class="tip">'
|
919
|
output << view.render_issue_tooltip(options[:issue])
|
920
|
output << "</span></div>"
|
921
|
|
922
|
output << view.draggable_element("ev_#{options[:kind]}#{options[:id]}", :revert =>false, :scroll=>"'gantt-container'", :constraint => "'horizontal'", :snap=>params[:zoom],:onEnd=>'function( draggable, event ) {issue_moved(draggable.element);}')
|
923
|
end
|
924
|
@lines << output
|
925
|
output
|
926
|
end
|
927
|
|
928
|
def pdf_task(params, coords, options={})
|
929
|
height = options[:height] || 2
|
930
|
|
931
|
# Renders the task bar, with progress and late
|
932
|
if coords[:bar_start] && coords[:bar_end]
|
933
|
params[:pdf].SetY(params[:top]+1.5)
|
934
|
params[:pdf].SetX(params[:subject_width] + coords[:bar_start])
|
935
|
params[:pdf].SetFillColor(200,200,200)
|
936
|
params[:pdf].Cell(coords[:bar_end] - coords[:bar_start], height, "", 0, 0, "", 1)
|
937
|
|
938
|
if coords[:bar_late_end]
|
939
|
params[:pdf].SetY(params[:top]+1.5)
|
940
|
params[:pdf].SetX(params[:subject_width] + coords[:bar_start])
|
941
|
params[:pdf].SetFillColor(255,100,100)
|
942
|
params[:pdf].Cell(coords[:bar_late_end] - coords[:bar_start], height, "", 0, 0, "", 1)
|
943
|
end
|
944
|
if coords[:bar_progress_end]
|
945
|
params[:pdf].SetY(params[:top]+1.5)
|
946
|
params[:pdf].SetX(params[:subject_width] + coords[:bar_start])
|
947
|
params[:pdf].SetFillColor(90,200,90)
|
948
|
params[:pdf].Cell(coords[:bar_progress_end] - coords[:bar_start], height, "", 0, 0, "", 1)
|
949
|
end
|
950
|
end
|
951
|
# Renders the markers
|
952
|
if options[:markers]
|
953
|
if coords[:start]
|
954
|
params[:pdf].SetY(params[:top] + 1)
|
955
|
params[:pdf].SetX(params[:subject_width] + coords[:start] - 1)
|
956
|
params[:pdf].SetFillColor(50,50,200)
|
957
|
params[:pdf].Cell(2, 2, "", 0, 0, "", 1)
|
958
|
end
|
959
|
if coords[:end]
|
960
|
params[:pdf].SetY(params[:top] + 1)
|
961
|
params[:pdf].SetX(params[:subject_width] + coords[:end] - 1)
|
962
|
params[:pdf].SetFillColor(50,50,200)
|
963
|
params[:pdf].Cell(2, 2, "", 0, 0, "", 1)
|
964
|
end
|
965
|
end
|
966
|
# Renders the label on the right
|
967
|
if options[:label]
|
968
|
params[:pdf].SetX(params[:subject_width] + (coords[:bar_end] || 0) + 5)
|
969
|
params[:pdf].Cell(30, 2, options[:label])
|
970
|
end
|
971
|
end
|
972
|
|
973
|
def image_task(params, coords, options={})
|
974
|
height = options[:height] || 6
|
975
|
|
976
|
# Renders the task bar, with progress and late
|
977
|
if coords[:bar_start] && coords[:bar_end]
|
978
|
params[:image].fill('#aaa')
|
979
|
params[:image].rectangle(params[:subject_width] + coords[:bar_start], params[:top], params[:subject_width] + coords[:bar_end], params[:top] - height)
|
980
|
|
981
|
if coords[:bar_late_end]
|
982
|
params[:image].fill('#f66')
|
983
|
params[:image].rectangle(params[:subject_width] + coords[:bar_start], params[:top], params[:subject_width] + coords[:bar_late_end], params[:top] - height)
|
984
|
end
|
985
|
if coords[:bar_progress_end]
|
986
|
params[:image].fill('#00c600')
|
987
|
params[:image].rectangle(params[:subject_width] + coords[:bar_start], params[:top], params[:subject_width] + coords[:bar_progress_end], params[:top] - height)
|
988
|
end
|
989
|
end
|
990
|
# Renders the markers
|
991
|
if options[:markers]
|
992
|
if coords[:start]
|
993
|
x = params[:subject_width] + coords[:start]
|
994
|
y = params[:top] - height / 2
|
995
|
params[:image].fill('blue')
|
996
|
params[:image].polygon(x-4, y, x, y-4, x+4, y, x, y+4)
|
997
|
end
|
998
|
if coords[:end]
|
999
|
x = params[:subject_width] + coords[:end] + params[:zoom]
|
1000
|
y = params[:top] - height / 2
|
1001
|
params[:image].fill('blue')
|
1002
|
params[:image].polygon(x-4, y, x, y-4, x+4, y, x, y+4)
|
1003
|
end
|
1004
|
end
|
1005
|
# Renders the label on the right
|
1006
|
if options[:label]
|
1007
|
params[:image].fill('black')
|
1008
|
params[:image].text(params[:subject_width] + (coords[:bar_end] || 0) + 5,params[:top] + 1, options[:label])
|
1009
|
end
|
1010
|
end
|
1011
|
|
1012
|
##
|
1013
|
## for edit gantt
|
1014
|
##
|
1015
|
def set_project_data(prj, zoom, text, prj_map = {})
|
1016
|
if !prj
|
1017
|
return text
|
1018
|
end
|
1019
|
if !prj_map[prj.id]
|
1020
|
o = get_project_position(prj, zoom)
|
1021
|
text += "|p#{prj.id}=#{format_date(prj.start_date)},#{prj.start_date},#{format_date(prj.due_date)},#{prj.due_date},#{o[0]},#{o[1]},#{o[2]},#{o[3]},#{o[4]},#{o[5]}"
|
1022
|
prj_map[prj.id] = prj
|
1023
|
end
|
1024
|
text = set_project_data(prj.parent, zoom, text, prj_map)
|
1025
|
end
|
1026
|
|
1027
|
def set_version_data(version, zoom, text, version_map = {})
|
1028
|
if !version
|
1029
|
return text
|
1030
|
end
|
1031
|
if !version_map[version.id]
|
1032
|
o = get_version_position(version, zoom)
|
1033
|
text += "|v#{version.id}=#{format_date(version.start_date)},#{version.start_date},#{format_date(version.due_date)},#{version.due_date},#{o[0]},#{o[1]},#{o[2]},#{o[3]},#{o[4]},#{o[5]}"
|
1034
|
version_map[version.id] = version
|
1035
|
end
|
1036
|
return text
|
1037
|
end
|
1038
|
|
1039
|
def get_pos(coords)
|
1040
|
i_left = 0
|
1041
|
i_width = 0
|
1042
|
l_width = 0
|
1043
|
d_width = 0
|
1044
|
if coords[:bar_start]
|
1045
|
i_left = coords[:bar_start]
|
1046
|
if coords[:bar_end]
|
1047
|
i_width = coords[:bar_end] - coords[:bar_start] - 2
|
1048
|
i_width = 0 if i_width < 0
|
1049
|
end
|
1050
|
if coords[:bar_late_end]
|
1051
|
l_width = coords[:bar_late_end] - coords[:bar_start] - 2
|
1052
|
end
|
1053
|
if coords[:bar_progress_end]
|
1054
|
d_width = coords[:bar_progress_end] - coords[:bar_start] - 2
|
1055
|
end
|
1056
|
end
|
1057
|
return i_left, i_width, l_width, d_width
|
1058
|
end
|
1059
|
|
1060
|
def get_issue_position(issue, zoom_str)
|
1061
|
z = zoom_str.to_i
|
1062
|
zoom = 1
|
1063
|
z.times { zoom = zoom * 2}
|
1064
|
id = issue.due_before
|
1065
|
if id && @date_to < id
|
1066
|
id = @date_to
|
1067
|
end
|
1068
|
coords = coordinates(issue.start_date, id, issue.done_ratio, zoom)
|
1069
|
|
1070
|
return get_pos(coords)
|
1071
|
end
|
1072
|
|
1073
|
def get_project_position(project, zoom_str)
|
1074
|
z = zoom_str.to_i
|
1075
|
zoom = 1
|
1076
|
z.times { zoom = zoom * 2}
|
1077
|
pd = project.due_date
|
1078
|
if pd && @date_to < pd
|
1079
|
pd = @date_to
|
1080
|
end
|
1081
|
coords = coordinates(project.start_date, pd, nil, zoom)
|
1082
|
i_left, i_width, l_width, d_width = get_pos(coords)
|
1083
|
if coords[:end]
|
1084
|
return i_left, i_width, l_width, d_width, coords[:start], coords[:end] + zoom
|
1085
|
else
|
1086
|
return i_left, i_width, l_width, d_width, coords[:start], nil
|
1087
|
end
|
1088
|
end
|
1089
|
|
1090
|
def get_version_position(version, zoom_str)
|
1091
|
z = zoom_str.to_i
|
1092
|
zoom = 1
|
1093
|
z.times { zoom = zoom * 2}
|
1094
|
vd = version.due_date
|
1095
|
if vd && @date_to < vd
|
1096
|
vd = @date_to
|
1097
|
end
|
1098
|
coords = coordinates(version.start_date, vd, version.completed_pourcent, zoom)
|
1099
|
i_left, i_width, l_width, d_width = get_pos(coords)
|
1100
|
if coords[:end]
|
1101
|
return i_left, i_width, l_width, d_width, coords[:start], coords[:end] + zoom
|
1102
|
else
|
1103
|
return i_left, i_width, l_width, d_width, coords[:start], nil
|
1104
|
end
|
1105
|
end
|
1106
|
|
1107
|
def calendar_for_issue(issue, options)
|
1108
|
# Skip issues that don't have a due_before (due_date or version's due_date)
|
1109
|
if issue.is_a?(Issue) && issue.due_before
|
1110
|
|
1111
|
case options[:format]
|
1112
|
when :html
|
1113
|
start_date = issue.start_date
|
1114
|
if start_date
|
1115
|
@calendars << "<div style='position: absolute;line-height:1.2em;height:16px;top:#{options[:top]}px;left:4px;overflow:hidden;'>"
|
1116
|
@calendars << "<span id='i#{issue.id}_start_date_str'>"
|
1117
|
@calendars << format_date(start_date)
|
1118
|
@calendars << "</span>"
|
1119
|
@calendars << "<input type='hidden' size='12' id='i#{issue.id}_hidden_start_date' value='#{start_date}' />"
|
1120
|
@calendars << "<input type='hidden' size='12' id='i#{issue.id}_start_date' value='#{start_date}'>#{view.g_calendar_for('i' + issue.id.to_s + '_start_date')}"
|
1121
|
@calendars << observe_date_field("i#{issue.id}", 'start')
|
1122
|
end
|
1123
|
due_date = issue.due_date
|
1124
|
if due_date
|
1125
|
@calendars << "<span id='i#{issue.id}_due_date_str'>"
|
1126
|
@calendars << format_date(due_date)
|
1127
|
@calendars << "</span>"
|
1128
|
@calendars << "<input type='hidden' size='12' id='i#{issue.id}_hidden_due_date' value='#{due_date}' />"
|
1129
|
@calendars << "<input type='hidden' size='12' id='i#{issue.id}_due_date' value='#{due_date}'>#{view.g_calendar_for('i' + issue.id.to_s + '_due_date')}"
|
1130
|
@calendars << observe_date_field("i#{issue.id}", 'due')
|
1131
|
@calendars << "</div>"
|
1132
|
end
|
1133
|
when :image
|
1134
|
#nop
|
1135
|
when :pdf
|
1136
|
#nop
|
1137
|
end
|
1138
|
else
|
1139
|
ActiveRecord::Base.logger.debug "GanttHelper#line_for_issue was not given an issue with a due_before"
|
1140
|
''
|
1141
|
end
|
1142
|
end
|
1143
|
|
1144
|
def calendar_for_version(version, options)
|
1145
|
# Skip version that don't have a due_before (due_date or version's due_date)
|
1146
|
if version.is_a?(Version) && version.start_date && version.due_date
|
1147
|
|
1148
|
case options[:format]
|
1149
|
when :html
|
1150
|
@calendars << "<div style='position: absolute;line-height:1.2em;height:16px;top:#{options[:top]}px;left:4px;overflow:hidden;'>"
|
1151
|
@calendars << "<span id='v#{version.id}_start_date_str'>"
|
1152
|
@calendars << format_date(version.effective_date)
|
1153
|
@calendars << "</span>"
|
1154
|
@calendars << "</div>"
|
1155
|
when :image
|
1156
|
#nop
|
1157
|
when :pdf
|
1158
|
#nop
|
1159
|
end
|
1160
|
else
|
1161
|
ActiveRecord::Base.logger.debug "GanttHelper#line_for_issue was not given an issue with a due_before"
|
1162
|
''
|
1163
|
end
|
1164
|
end
|
1165
|
|
1166
|
def calendar_for_project(project, options)
|
1167
|
case options[:format]
|
1168
|
when :html
|
1169
|
@calendars << "<div style='position: absolute;line-height:1.2em;height:16px;top:#{options[:top]}px;left:4px;overflow:hidden;'>"
|
1170
|
@calendars << "<span id='p#{project.id}_start_date_str'>"
|
1171
|
@calendars << format_date(project.start_date) if project.start_date
|
1172
|
@calendars << "</span>"
|
1173
|
@calendars << " "
|
1174
|
@calendars << "<span id='p#{project.id}_due_date_str'>"
|
1175
|
@calendars << format_date(project.due_date) if project.due_date
|
1176
|
@calendars << "</span>"
|
1177
|
@calendars << "</div>"
|
1178
|
when :image
|
1179
|
# nop
|
1180
|
when :pdf
|
1181
|
# nop
|
1182
|
end
|
1183
|
end
|
1184
|
|
1185
|
def observe_date_field(id, type)
|
1186
|
output = ''
|
1187
|
prj_id = ''
|
1188
|
prj_id = @project.to_param if @project
|
1189
|
output << "<script type='text/javascript'>\n"
|
1190
|
output << "//<![CDATA[\n"
|
1191
|
output << "new Form.Element.Observer('#{id}_#{type}_date', 0.25,\n"
|
1192
|
output << " function(element, value) {\n"
|
1193
|
output << " if (value == document.getElementById('#{id}_hidden_#{type}_date').value) {\n"
|
1194
|
output << " return ;\n"
|
1195
|
output << " }\n"
|
1196
|
output << " new Ajax.Request('#{view.url_for(:controller=>:gantts, :action => :edit_gantt, :id=>id, :date_from=>self.date_from.strftime("%Y-%m-%d"), :date_to=>self.date_to.strftime("%Y-%m-%d"), :zoom=>self.zoom, :escape => false, :project_id=>prj_id)}', {asynchronous:true, evalScripts:true, onFailure:function(request){handle_failure(request.responseText)}, onSuccess:function(request){change_dates(request.responseText)}, parameters:'#{type}_date=' + encodeURIComponent(value)});"
|
1197
|
output << " })\n"
|
1198
|
output << "//]]>\n"
|
1199
|
output << "</script>"
|
1200
|
end
|
1201
|
end
|
1202
|
end
|
1203
|
end
|