Project

General

Profile

Feature #2024 » gantt.rb

Masayuki Shibata, 2011-04-22 13:57

 
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 += "-&nbsp;<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'>&nbsp;</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"}'>&nbsp;</div>"
883
          else
884
            output << "<div id='task_late_#{options[:kind]}#{options[:id]}' style='float:left:0px; width:0px;' class='#{ options[:css] + " task_none"}'>&nbsp;</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"}'>&nbsp;</div>"
889
          else
890
            output << "<div id='task_done_#{options[:kind]}#{options[:id]}' style='float:left:0px; width:0px;' class='#{ options[:css] + " task_none"}'>&nbsp;</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'>&nbsp;</div>"
896
          output << "<div id='task_late_#{options[:kind]}#{options[:id]}' style='float:left:0px; width:0px;' class='#{ options[:css] + " task_none"}'>&nbsp;</div>"
897
          output << "<div id='task_done_#{options[:kind]}#{options[:id]}' style='float:left:0px; width:0px;' class='#{ options[:css] + " task_none"}'>&nbsp;</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'>&nbsp;</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'>&nbsp;</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 << "&nbsp;&nbsp;&nbsp;"
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
(16-16/35)