Project

General

Profile

Patch #12730 » gantt_diagram.diff

Patch for Gantt diagram - Tobias Droste, 2013-01-04 10:08

View differences:

trunk/app/controllers/gantts_controller.rb
34 34
    @gantt = Redmine::Helpers::Gantt.new(params)
35 35
    @gantt.project = @project
36 36
    retrieve_query
37
    @query.group_by = nil
38 37
    @gantt.query = @query if @query.valid?
39 38

  
40 39
    basename = (@project ? "#{@project.identifier}-" : '') + 'gantt'
trunk/app/views/gantts/show.html.erb
1 1
<% @gantt.view = self %>
2 2
<h2><%= @query.new_record? ? l(:label_gantt) : h(@query.name) %></h2>
3 3

  
4
<% if @query.valid? %>
5
  <%- @gantt.setup_dates %>
6
<% end %>
7

  
4 8
<%= form_tag({:controller => 'gantts', :action => 'show',
5 9
             :project_id => @project, :month => params[:month],
6
             :year => params[:year], :months => params[:months]},
10
             :year => params[:year], :months => params[:months],
11
             :group_filter => params[:group_filter]},
7 12
             :method => :get, :id => 'query_form') do %>
8 13
<%= hidden_field_tag 'set_filter', '1' %>
9 14
<fieldset id="filters" class="collapsible <%= @query.new_record? ? "" : "collapsed" %>">
10 15
  <legend onclick="toggleFieldset(this);"><%= l(:label_filter_plural) %></legend>
11 16
  <div style="<%= @query.new_record? ? "" : "display: none;" %>">
12 17
    <%= render :partial => 'queries/filters', :locals => {:query => @query} %>
18
    <table>
19
      <tr>
20
        <td class="field"><label for='group_by'><%= l(:field_group_by) %></label></td>
21
        <td><%= select_tag('group_by', options_for_select(
22
                  [[]] + @query.groupable_columns.collect {|c| [c.caption, c.name.to_s]},
23
                  @query.group_by)) %></td>
24
      </tr>
25
    </table>
13 26
  </div>
14 27
</fieldset>
28
<fieldset class="collapsible collapsed" style="display: none;">
29
  <legend onclick="toggleFieldset(this);"><%= l(:label_options) %></legend>
30
  <div style="display: none;">
31
	<table>
32
	  <tr>
33
	    <td><%= l(:field_column_names) %></td>
34
	    <td><%= render :partial => 'queries/columns', :locals => {:query => @query} %></td>
35
	  </tr>
36
	</table>
37
  </div>
38
</fieldset>
15 39

  
16 40
<p class="contextual">
17 41
  <%= gantt_zoom_link(@gantt, :in) %>
......
29 53
                     :class => 'icon icon-checked' %>
30 54
<%= link_to l(:button_clear), { :project_id => @project, :set_filter => 1 },
31 55
            :class => 'icon icon-reload' %>
56
<% if @query.new_record? && User.current.allowed_to?(:save_queries, @project, :global => true) %>
57
	<%= link_to_function l(:button_save),
58
		             "$('query_form').action='#{ @project ? new_project_query_path(@project) : new_query_path }'; submit_query_form('query_form')",
59
                     :class => 'icon icon-save' %>
60
<% end %>
32 61
</p>
33 62
<% end %>
34 63

  
trunk/lib/redmine/helpers/gantt.rb
42 42

  
43 43
      def initialize(options={})
44 44
        options = options.dup
45
        if options[:year] && options[:year].to_i >0
45

  
46
        if options[:year] && options[:year].to_i > 0
46 47
          @year_from = options[:year].to_i
47
          if options[:month] && options[:month].to_i >=1 && options[:month].to_i <= 12
48
          if options[:month] && options[:month].to_i >= 1 && options[:month].to_i <= 12
48 49
            @month_from = options[:month].to_i
49 50
          else
50 51
            @month_from = 1
51 52
          end
52 53
        else
53
          @month_from ||= Date.today.month
54
          @month_from ||= Date.today.month - 1
54 55
          @year_from ||= Date.today.year
56
          @calculate_date_range = true
55 57
        end
58

  
59
        if @month_from <= 0
60
          @month_from = 12
61
          @year_from -= 1
62
        end
63
        if @month_from > 12
64
          @month_from = 1
65
          @year_from += 1
66
        end
67
        
68
        months = (options[:months] || User.current.pref[:gantt_months]).to_i
69
        @months = (months > 0 && months < 48) ? months : 48
70
        
56 71
        zoom = (options[:zoom] || User.current.pref[:gantt_zoom]).to_i
57 72
        @zoom = (zoom > 0 && zoom < 5) ? zoom : 2
58
        months = (options[:months] || User.current.pref[:gantt_months]).to_i
59
        @months = (months > 0 && months < 25) ? months : 6
73

  
60 74
        # Save gantt parameters as user preference (zoom and months count)
61 75
        if (User.current.logged? && (@zoom != User.current.pref[:gantt_zoom] ||
62 76
              @months != User.current.pref[:gantt_months]))
63 77
          User.current.pref[:gantt_zoom], User.current.pref[:gantt_months] = @zoom, @months
64 78
          User.current.preference.save
65 79
        end
80

  
66 81
        @date_from = Date.civil(@year_from, @month_from, 1)
67 82
        @date_to = (@date_from >> @months) - 1
68 83
        @subjects = ''
......
70 85
        @number_of_rows = nil
71 86
        @issue_ancestors = []
72 87
        @truncated = false
88

  
73 89
        if options.has_key?(:max_rows)
74 90
          @max_rows = options[:max_rows]
75 91
        else
......
77 93
        end
78 94
      end
79 95

  
96
      def setup_dates
97
        if @calculate_date_range
98
          dates = issues_date_range
99

  
100
          @date_from = dates[0]
101
          if @date_from.day < 10
102
            @date_from = dates[0] << 1
103
          end
104
          @date_to = dates[1]
105
          if @date_to.day > 20
106
            @date_to = dates[1] >> 1
107
          end
108
          @year_from = @date_from.year
109
          @month_from = @date_from.month
110
          months = (@date_to.year*12+@date_to.month) - (@date_from.year*12+@date_from.month) + 1
111
          @months = months > 48 ? 48 : months
112
          @date_from = Date.civil(@year_from, @month_from, 1)
113
          @date_to = (@date_from >> @months) - 1
114
        end
115
      end
116

  
80 117
      def common_params
81 118
        { :controller => 'gantts', :action => 'show', :project_id => @project }
82 119
      end
......
209 246

  
210 247
      def render_issues(issues, options={})
211 248
        @issue_ancestors = []
212
        issues.each do |i|
213
          subject_for_issue(i, options) unless options[:only] == :lines
214
          line_for_issue(i, options) unless options[:only] == :subjects
215
          options[:top] += options[:top_increment]
216
          @number_of_rows += 1
217
          break if abort?
249
        if @query.group_by == nil || @query.group_by == ""
250
          issues.each do |i|
251
            subject_for_issue(i, options) unless options[:only] == :lines
252
            line_for_issue(i, options) unless options[:only] == :subjects
253
            options[:top] += options[:top_increment]
254
            @number_of_rows += 1
255
            break if abort?
256
          end
257
          options[:indent] -= (options[:indent_increment] * @issue_ancestors.size)
258
        else
259
          new_group = true
260
          group_start = options[:top] - options[:top_increment]
261
          group_max_line = 0
262
          group_write_line = group_max_line
263
          dates_in_line = [[]]
264
          last_shown_index = -1
265

  
266
          issues.each_with_index do |i, index|
267
            if i.start_date != nil
268
              if i.due_before == nil
269
                i.due_date = i.start_date
270
              end
271

  
272
              if ((i.start_date >= @date_from || i.due_before >= @date_from) && i.start_date <= @date_to)
273
                new_group = last_shown_index < 0 || new_group?(i, issues[last_shown_index]) || !i.leaf? || !issues[last_shown_index].leaf?
274

  
275
                if new_group
276
                  options[:top] = group_start + ((group_max_line+1) * options[:top_increment])
277
                  group_start = options[:top]
278
                  group_max_line = 0
279
                  group_write_line = group_max_line
280
                  dates_in_line = [[]]
281
                  @number_of_rows += 1
282
                else
283
                  group_write_line = -1
284
                  dates_in_line.each_with_index do |dates, line|
285
                    if dates.find {|e| (i.start_date >= e[0] && i.start_date <= e[1]) || (i.start_date <= e[0] && i.due_before >= e[1]) } == nil
286
                      group_write_line = line
287
                      break
288
                    end
289
                  end
290
                  if group_write_line == -1
291
                    group_max_line += 1
292
                    group_write_line = group_max_line
293
                    dates_in_line.push([])
294
                    @number_of_rows += 1
295
                  end
296
                end
297

  
298
                options[:top] = group_start + (group_write_line * options[:top_increment])
299
                options[:no_title] = !new_group;
300

  
301
                subject_for_issue(i, options) unless options[:only] == :lines
302
                line_for_issue(i, options) unless options[:only] == :subjects
303

  
304
                last_shown_index = index
305
                dates_in_line[group_write_line].push([i.start_date, i.due_before])
306
              end
307
            end
308
            break if abort?
309
          end
310
          options[:top] = group_start + ((group_max_line+1) * options[:top_increment])
311
          options[:indent] -= (options[:indent_increment] * @issue_ancestors.size)
218 312
        end
219
        options[:indent] -= (options[:indent_increment] * @issue_ancestors.size)
220 313
      end
221 314

  
222 315
      def render_version(project, version, options={})
......
330 423
          @issue_ancestors.pop
331 424
          options[:indent] -= options[:indent_increment]
332 425
        end
426
        
427
        is_not_leaf_or_grouped = !issue.leaf? || @query.group_by == nil || @query.group_by == ""
428
        group_name = group_name(issue)
429

  
430
        if is_not_leaf_or_grouped
431
          subject_title = issue.subject
432
        else
433
          subject_title = group_name
434
        end
435

  
436
        if options[:no_title]
437
          subject_title = " "
438
        end
439
        
333 440
        output = case options[:format]
334 441
        when :html
335 442
          css_classes = ''
336 443
          css_classes << ' issue-overdue' if issue.overdue?
337 444
          css_classes << ' issue-behind-schedule' if issue.behind_schedule?
338
          css_classes << ' icon icon-issue' unless Setting.gravatar_enabled? && issue.assigned_to
445
          css_classes << ' icon icon-issue' unless Setting.gravatar_enabled? && issue.assigned_to || options[:no_title]
446
          
339 447
          s = "".html_safe
340
          if issue.assigned_to.present?
448
          if !options[:no_title] && issue.assigned_to.present?
341 449
            assigned_string = l(:field_assigned_to) + ": " + issue.assigned_to.name
342 450
            s << view.avatar(issue.assigned_to,
343 451
                             :class => 'gravatar icon-gravatar',
344 452
                             :size => 10,
345 453
                             :title => assigned_string).to_s.html_safe
346 454
          end
347
          s << view.link_to_issue(issue).html_safe
455
          
456
          if is_not_leaf_or_grouped
457
            s << view.link_to_issue(issue).html_safe
458
          else
459
            s << subject_title.html_safe
460
          end
461
          
348 462
          subject = view.content_tag(:span, s, :class => css_classes).html_safe
349 463
          html_subject(options, subject, :css => "issue-subject",
350
                       :title => issue.subject) + "\n"
464
                       :title => subject_title) + "\n"
351 465
        when :image
352
          image_subject(options, issue.subject)
466
          image_subject(options, subject_title)
353 467
        when :pdf
354 468
          pdf_new_page?(options)
355
          pdf_subject(options, issue.subject)
469
          pdf_subject(options, subject_title)
356 470
        end
471
        
357 472
        unless issue.leaf?
358 473
          @issue_ancestors << issue
359 474
          options[:indent] += options[:indent_increment]
360 475
        end
476
        
361 477
        output
362 478
      end
363 479

  
364 480
      def line_for_issue(issue, options)
365
        # Skip issues that don't have a due_before (due_date or version's due_date)
366
        if issue.is_a?(Issue) && issue.due_before
367
          coords = coordinates(issue.start_date, issue.due_before, issue.done_ratio, options[:zoom])
368
          label = "#{issue.status.name} #{issue.done_ratio}%"
481
        if issue.is_a?(Issue)
482
          due_date = issue.due_before
483
          if !due_date
484
            due_date = issue.start_date
485
          end
486

  
487
          coords = coordinates(issue.start_date, due_date, issue.done_ratio, options[:zoom])
488
          
489
          label = " "
490
          if @query.group_by == nil || @query.group_by == ""
491
            label = "#{issue.status.name} #{issue.done_ratio}%"
492
          end
493

  
369 494
          case options[:format]
370 495
          when :html
371 496
            html_task(options, coords,
......
376 501
            image_task(options, coords, :label => label)
377 502
          when :pdf
378 503
            pdf_task(options, coords, :label => label)
379
        end
504
          end
380 505
        else
381
          ActiveRecord::Base.logger.debug "GanttHelper#line_for_issue was not given an issue with a due_before"
506
          ActiveRecord::Base.logger.debug "GanttHelper#line_for_issue was not given an issue"
382 507
          ''
383 508
        end
384 509
      end
......
640 765

  
641 766
      # Sorts a collection of issues by start_date, due_date, id for gantt rendering
642 767
      def sort_issues!(issues)
643
        issues.sort! { |a, b| gantt_issue_compare(a, b) }
768
        issues.sort! { |a, b| gantt_issue_compare(a, b, issues) }
644 769
      end
645 770

  
646
      # TODO: top level issues should be sorted by start date
647
      def gantt_issue_compare(x, y)
648
        if x.root_id == y.root_id
649
          x.lft <=> y.lft
771
      def gantt_issue_compare(x, y, issues)
772
        if @query.group_by == nil || @query.group_by == ""
773
          [(x.root.start_date or x.start_date or Date.new()), x.root_id, (x.start_date or Date.new()), x.lft] <=> [(y.root.start_date or y.start_date or Date.new()), y.root_id, (y.start_date or Date.new()), y.lft]
774
        elsif x.leaf? && !y.leaf?
775
          1
776
        elsif !x.leaf? && y.leaf?
777
          -1
650 778
        else
651
          x.root_id <=> y.root_id
779
          sort_group(x, y, issues)
652 780
        end
653 781
      end
782
      
783
      def sort_group(x, y, issues)
784
        value_x = @query.group_by_column.value(x)
785
        value_y = @query.group_by_column.value(y)
654 786

  
787
        if value_x == nil && value_y == nil
788
          [(x.root.start_date or x.start_date or Date.new()), x.root_id, (x.start_date or Date.new()), x.lft] <=> [(y.root.start_date or y.start_date or Date.new()), y.root_id, (y.start_date or Date.new()), y.lft]
789
        elsif value_x == nil
790
          -1
791
        elsif value_y == nil
792
          1
793
        elsif value_x == value_y
794
          [(x.root.start_date or x.start_date or Date.new()), x.root_id, (x.start_date or Date.new()), x.lft] <=> [(y.root.start_date or y.start_date or Date.new()), y.root_id, (y.start_date or Date.new()), y.lft]
795
        else
796
          value_x <=> value_y
797
        end
798
      end
799
      
800
      def issues_date_range
801
        min = nil
802
        max = nil
803
        projects.each do |p|
804
          all_issues = project_issues(p)
805

  
806
          all_issues.each do |i|
807
            if min == nil || (i.start_date != nil && min > i.start_date)
808
              min = i.start_date
809
            end
810
            if min == nil || (i.due_before != nil && min > i.due_before)
811
              min = i.due_before
812
            end
813
            if max == nil || (i.start_date != nil && max < i.start_date)
814
              max = i.start_date
815
            end
816
            if max == nil || (i.due_before != nil && max < i.due_before)
817
              max = i.due_before
818
            end
819
          end
820
        end
821

  
822
        if min == nil && max == nil
823
          min = Date.today
824
          max = Date.today
825
        end
826

  
827
        [min, max]
828
      end
829
      
830
      def new_group?(x, y)
831
        if @query.group_by == nil || @query.group_by == ""
832
          true
833
        else
834
          value_x = @query.group_by_column.value(x)
835
          value_y = @query.group_by_column.value(y)
836

  
837
          value_x != value_y
838
        end
839
      end
840

  
841
      def group_name(issue)
842
        if @query.group_by == nil || @query.group_by == ""
843
          issue.subject
844
        else
845
          result = @query.group_by_column.value(issue)
846

  
847
          if result == nil || result == ''
848
            'None'
849
          else
850
            result.to_s
851
          end
852
        end
853
      end
854

  
655 855
      def current_limit
656 856
        if @max_rows
657 857
          @max_rows - @number_of_rows
......
790 990

  
791 991
      def pdf_task(params, coords, options={})
792 992
        height = options[:height] || 2
993
        
793 994
        # Renders the task bar, with progress and late
794 995
        if coords[:bar_start] && coords[:bar_end]
795
          params[:pdf].SetY(params[:top] + 1.5)
796
          params[:pdf].SetX(params[:subject_width] + coords[:bar_start])
996
          top = params[:top] + 1.5
997
          width = params[:subject_width] + coords[:bar_start]
998
          
999
          length = coords[:bar_end] - coords[:bar_start]
1000
          if length <= 0
1001
            length = 1
1002
          end
1003
          params[:pdf].SetY(top)
1004
          params[:pdf].SetX(width)
797 1005
          params[:pdf].SetFillColor(200, 200, 200)
798
          params[:pdf].RDMCell(coords[:bar_end] - coords[:bar_start], height, "", 0, 0, "", 1)
1006
          params[:pdf].RDMCell(length, height, "", 0, 0, "", 1)
1007
          
799 1008
          if coords[:bar_late_end]
800
            params[:pdf].SetY(params[:top] + 1.5)
801
            params[:pdf].SetX(params[:subject_width] + coords[:bar_start])
1009
            length = coords[:bar_late_end] - coords[:bar_start]
1010
            if length <= 0
1011
              length = 1
1012
            end
1013
            params[:pdf].SetY(top)
1014
            params[:pdf].SetX(width)
802 1015
            params[:pdf].SetFillColor(255, 100, 100)
803
            params[:pdf].RDMCell(coords[:bar_late_end] - coords[:bar_start], height, "", 0, 0, "", 1)
1016
            params[:pdf].RDMCell(length, height, "", 0, 0, "", 1)
804 1017
          end
1018
          
805 1019
          if coords[:bar_progress_end]
806
            params[:pdf].SetY(params[:top] + 1.5)
807
            params[:pdf].SetX(params[:subject_width] + coords[:bar_start])
1020
            length = coords[:bar_progress_end] - coords[:bar_start]
1021
            if length <= 0
1022
              length = 1
1023
            end
1024
            params[:pdf].SetY(top)
1025
            params[:pdf].SetX(width)
808 1026
            params[:pdf].SetFillColor(90, 200, 90)
809
            params[:pdf].RDMCell(coords[:bar_progress_end] - coords[:bar_start], height, "", 0, 0, "", 1)
1027
            params[:pdf].RDMCell(length, height, "", 0, 0, "", 1)
810 1028
          end
811 1029
        end
1030
        
812 1031
        # Renders the markers
813 1032
        if options[:markers]
814 1033
          if coords[:start]
......
824 1043
            params[:pdf].RDMCell(2, 2, "", 0, 0, "", 1)
825 1044
          end
826 1045
        end
1046
        
827 1047
        # Renders the label on the right
828 1048
        if options[:label]
829 1049
          params[:pdf].SetX(params[:subject_width] + (coords[:bar_end] || 0) + 5)
(1-1/4)