Project

General

Profile

Feature #347 » Add-grouping-functionality-to-Gantt-diagram.patch

Patch to add grouping to Gantt diagram - Tobias Droste, 2013-01-05 09:23

View differences:

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'
app/views/gantts/show.html.erb
7 7

  
8 8
<%= form_tag({:controller => 'gantts', :action => 'show',
9 9
             :project_id => @project, :month => params[:month],
10
             :year => params[:year], :months => params[:months]},
10
             :year => params[:year], :months => params[:months],
11
             :not_detailed_groups => params[:not_detailed_groups],
12
             :version_groups => params[:version_groups]},
11 13
             :method => :get, :id => 'query_form') do %>
12 14
<%= hidden_field_tag 'set_filter', '1' %>
13 15
<fieldset id="filters" class="collapsible <%= @query.new_record? ? "" : "collapsed" %>">
......
16 18
    <%= render :partial => 'queries/filters', :locals => {:query => @query} %>
17 19
  </div>
18 20
</fieldset>
21
<fieldset class="collapsible collapsed" style="display: none;">
22
  <legend onclick="toggleFieldset(this);"><%= l(:label_options) %></legend>
23
  <div style="display: none;">
24
    <table>
25
      <tr>
26
        <td><%= l(:field_column_names) %></td>
27
        <td><%= render :partial => 'queries/columns', :locals => {:query => @query} %></td>
28
      </tr>
29
    </table>
30
  </div>
31
</fieldset>
32
<fieldset class="collapsible collapsed">
33
  <legend onclick="toggleFieldset(this);"><%= l(:label_options) %></legend>
34
  <div style="display: none;">
35
    <table>
36
      <tr>
37
        <td><label for='group_by'><%= l(:field_group_by) %></label></td>
38
        <td><%= select_tag('group_by',
39
                           options_for_select(
40
                             [[]] + @query.groupable_columns.collect {|c| [c.caption, c.name.to_s]},
41
                             @query.group_by)
42
                   ) %></td>
43
      </tr>
44
      <tr>
45
        <td><%= l(:button_show) %></td>
46
        <td><%= content_tag('label', check_box_tag('not_detailed_groups', true, !@gantt.detailed_groups) + l(:label_hide_group_details), :class => 'inline') %></td>
47
      </tr>
48
      <tr>
49
        <td></td>
50
        <td><%= content_tag('label', check_box_tag('version_groups', true, @gantt.version_groups) + l(:label_keep_version_groups), :class => 'inline') %></td>
51
      </tr>
52
    </table>
53
  </div>
54
</fieldset>
19 55

  
20 56
<p class="contextual">
21 57
  <%= gantt_zoom_link(@gantt, :in) %>
......
33 69
                     :class => 'icon icon-checked' %>
34 70
<%= link_to l(:button_clear), { :project_id => @project, :set_filter => 1 },
35 71
            :class => 'icon icon-reload' %>
72
<% if @query.new_record? && User.current.allowed_to?(:save_queries, @project, :global => true) %>
73
    <%= link_to_function l(:button_save),
74
            "$('query_form').action='#{ @project ? new_project_query_path(@project) : new_query_path }'; submit_query_form('query_form')",
75
            :class => 'icon icon-save' %>
76
<% end %>
36 77
</p>
37 78
<% end %>
38 79

  
config/locales/de.yml
611 611
  label_module_plural: Module
612 612
  label_month: Monat
613 613
  label_months_from: Monate ab
614
  label_hide_group_details: Verstecke Gruppendetails
615
  label_keep_version_groups: Behalte Versiongruppen
614 616
  label_more: Mehr
615 617
  label_more_than_ago: vor mehr als
616 618
  label_my_account: Mein Konto
config/locales/en-GB.yml
587 587
  label_per_page: Per page
588 588
  label_calendar: Calendar
589 589
  label_months_from: months from
590
  label_hide_group_details: Hide group details
591
  label_keep_version_groups: Keep version groups
590 592
  label_gantt: Gantt
591 593
  label_internal: Internal
592 594
  label_last_changes: "last %{count} changes"
config/locales/en.yml
627 627
  label_per_page: Per page
628 628
  label_calendar: Calendar
629 629
  label_months_from: months from
630
  label_hide_group_details: Hide group details
631
  label_keep_version_groups: Keep version groups
630 632
  label_gantt: Gantt
631 633
  label_internal: Internal
632 634
  label_last_changes: "last %{count} changes"
lib/redmine/helpers/gantt.rb
36 36
      end
37 37

  
38 38
      attr_reader :year_from, :month_from, :date_from, :date_to, :zoom, :months, :truncated, :max_rows
39
      attr_reader :detailed_groups, :version_groups
39 40
      attr_accessor :query
40 41
      attr_accessor :project
41 42
      attr_accessor :view
......
64 65
          @month_from = 1
65 66
          @year_from += 1
66 67
        end
67
        
68

  
68 69
        months = (options[:months] || User.current.pref[:gantt_months]).to_i
69 70
        @months = (months > 0 && months < 48) ? months : 48
70
        
71

  
71 72
        zoom = (options[:zoom] || User.current.pref[:gantt_zoom]).to_i
72 73
        @zoom = (zoom > 0 && zoom < 5) ? zoom : 2
73 74

  
74
        # Save gantt parameters as user preference (zoom and months count)
75
        if (User.current.logged? && (@zoom != User.current.pref[:gantt_zoom] ||
76
              @months != User.current.pref[:gantt_months]))
77
          User.current.pref[:gantt_zoom], User.current.pref[:gantt_months] = @zoom, @months
75
        detailed_groups = User.current.pref[:gantt_detailed_groups]
76
        if options[:set_filter] == '1'
77
          detailed_groups = (options[:not_detailed_groups].to_s == 'true') ? false : true
78
        end
79
        @detailed_groups = (detailed_groups.to_s == 'true') ? true : false
80

  
81
        version_groups = User.current.pref[:gantt_version_groups].to_s
82
        if options[:set_filter] == '1'
83
          version_groups = options[:version_groups].to_s
84
        end
85
        @version_groups = (version_groups == 'true') ? true : false
86

  
87
        # Save gantt parameters as user preference (zoom, months count and detailed grouping)
88
        if (User.current.logged? &&
89
             (@zoom != User.current.pref[:gantt_zoom] ||
90
              @months != User.current.pref[:gantt_months] ||
91
              @detailed_groups != User.current.pref[:gantt_detailed_groups] ||
92
              @version_groups != User.current.pref[:gantt_version_groups]))
93
          User.current.pref[:gantt_zoom] = @zoom
94
          User.current.pref[:gantt_months] = @months
95
          User.current.pref[:gantt_detailed_groups] = @detailed_groups
96
          User.current.pref[:gantt_version_groups] = @version_groups
78 97
          User.current.preference.save
79 98
        end
80 99

  
......
120 139

  
121 140
      def params
122 141
        common_params.merge({:zoom => zoom, :year => year_from,
123
                             :month => month_from, :months => months})
142
                             :month => month_from, :months => months,
143
                             :not_detailed_groups => !detailed_groups,
144
                             :version_groups => version_groups})
124 145
      end
125 146

  
126 147
      def params_previous
127 148
        common_params.merge({:year => (date_from << months).year,
128 149
                             :month => (date_from << months).month,
129
                             :zoom => zoom, :months => months})
150
                             :zoom => zoom, :months => months,
151
                             :not_detailed_groups => !detailed_groups,
152
                             :version_groups => version_groups})
130 153
      end
131 154

  
132 155
      def params_next
133 156
        common_params.merge({:year => (date_from >> months).year,
134 157
                             :month => (date_from >> months).month,
135
                             :zoom => zoom, :months => months})
158
                             :zoom => zoom, :months => months,
159
                             :not_detailed_groups => !detailed_groups,
160
                             :version_groups => version_groups})
136 161
      end
137 162

  
138 163
      # Returns the number of rows that will be rendered on the Gantt chart
......
205 230
        project_issues(project).select {|issue| issue.fixed_version == version}
206 231
      end
207 232

  
233
      # Returns the issues that belong to +project+ and are grouped by +group+
234
      def group_issues!(group, issues)
235
        result = issues.take_while {|issue| group_name(issue) == group}
236
        issues.reject! {|issue| group_name(issue) == group}
237
        result
238
      end
239

  
208 240
      def render(options={})
209 241
        options = {:top => 0, :top_increment => 20,
210 242
                   :indent_increment => 20, :render => :subject,
......
230 262
        options[:indent] += options[:indent_increment]
231 263
        @number_of_rows += 1
232 264
        return if abort?
233
        issues = project_issues(project).select {|i| i.fixed_version.nil?}
234
        sort_issues!(issues)
235
        if issues
265
        issues = project_issues(project).select {|i| i.fixed_version.nil? || (grouped? && !@version_groups) }
266
        if issues && grouped?
267
          render_groups(issues, options)
268
          return if abort?
269
        else
270
          sort_issues!(issues)
236 271
          render_issues(issues, options)
237 272
          return if abort?
238 273
        end
......
245 280
        options[:indent] -= options[:indent_increment]
246 281
      end
247 282

  
248
      def render_issues(issues, options={})
249
        @issue_ancestors = []
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
      end
259

  
260 283
      def render_version(project, version, options={})
261 284
        # Version header
262 285
        subject_for_version(version, options) unless options[:only] == :lines
263 286
        line_for_version(version, options) unless options[:only] == :subjects
264 287
        options[:top] += options[:top_increment]
288
        options[:indent] += options[:indent_increment]
265 289
        @number_of_rows += 1
266
        return if abort?
290
        return if abort? || (grouped? && !@version_groups)
267 291
        issues = version_issues(project, version)
268
        if issues
292
        if issues && grouped?
293
          render_groups(issues, options)
294
          return if abort?
295
        else
269 296
          sort_issues!(issues)
270
          # Indent issues
271
          options[:indent] += options[:indent_increment]
272 297
          render_issues(issues, options)
273
          options[:indent] -= options[:indent_increment]
298
          return if abort?
274 299
        end
300
        # Remove indent to hit the next sibling
301
        options[:indent] -= options[:indent_increment]
302
      end
303

  
304
      def render_groups(issues, options={})
305
        while !issues.empty?
306
          # Group header
307
          group = group_name(issues[0])
308
          subject_for_group(group, options) unless options[:only] == :lines
309
          options[:top] += options[:top_increment]
310
          @number_of_rows += 1
311
          break if abort?
312
          gr_issues = group_issues!(group, issues)
313
          if gr_issues
314
            sort_issues!(gr_issues)
315
            # Indent issues
316
            options[:indent] += options[:indent_increment]
317
            render_issues(gr_issues, options)
318
            options[:indent] -= options[:indent_increment]
319
          end
320
          break if abort?
321
        end
322
      end
323

  
324
      def render_issues(issues, options={})
325
        @issue_ancestors = []
326
        if !grouped? || @detailed_groups
327
          issues.each do |i|
328
            if i.due_before == nil
329
              i.due_date = i.start_date
330
            end
331
            subject_for_issue(i, options) unless options[:only] == :lines
332
            line_for_issue(i, options) unless options[:only] == :subjects
333
            options[:top] += options[:top_increment]
334
            @number_of_rows += 1
335
            break if abort?
336
          end
337
        else
338
          group_start = options[:top] - options[:top_increment]
339
          group_max_line = 0
340
          group_write_line = group_max_line
341
          dates_in_line = [[]]
342
          @number_of_rows += 1
343
          issues.each do |i|
344
            if i.leaf?
345
              if i.due_before == nil
346
                i.due_date = i.start_date
347
              end
348
              group_write_line = -1
349
              dates_in_line.each_with_index do |dates, line|
350
                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
351
                  group_write_line = line
352
                  break
353
                end
354
              end
355
              if group_write_line == -1
356
                group_max_line += 1
357
                group_write_line = group_max_line
358
                dates_in_line.push([])
359
                @number_of_rows += 1
360
              end
361
              options[:top] = group_start + (group_write_line * options[:top_increment])
362
              subject_for_issue(i, options) unless options[:only] == :lines
363
              line_for_issue(i, options) unless options[:only] == :subjects
364
              dates_in_line[group_write_line].push([i.start_date, i.due_before])
365
            end
366
            break if abort?
367
          end
368
          options[:top] = group_start + ((group_max_line+1) * options[:top_increment])
369
        end
370
        options[:indent] -= (options[:indent_increment] * @issue_ancestors.size)
275 371
      end
276 372

  
277 373
      def render_end(options={})
......
300 396
      end
301 397

  
302 398
      def line_for_project(project, options)
303
        # Skip versions that don't have a start_date or due date
399
        # Skip projects that don't have a start_date or due date
304 400
        if project.is_a?(Project) && project.start_date && project.due_date
305 401
          options[:zoom] ||= 1
306 402
          options[:g_width] ||= (self.date_to - self.date_from + 1) * options[:zoom]
......
363 459
        end
364 460
      end
365 461

  
462
      def subject_for_group(group, options)
463
        case options[:format]
464
        when :html
465
          html_class = ""
466
          html_class << 'icon icon-package '
467
          s = group.html_safe
468
          subject = view.content_tag(:span, s,
469
                                     :class => html_class).html_safe
470
          html_subject(options, subject, :css => "version-name")
471
        when :image
472
          image_subject(options, group)
473
        when :pdf
474
          pdf_new_page?(options)
475
          pdf_subject(options, group)
476
        end
477
      end
478

  
366 479
      def subject_for_issue(issue, options)
367 480
        while @issue_ancestors.any? && !issue.is_descendant_of?(@issue_ancestors.last)
368 481
          @issue_ancestors.pop
......
370 483
        end
371 484
        output = case options[:format]
372 485
        when :html
373
          css_classes = ''
374
          css_classes << ' issue-overdue' if issue.overdue?
375
          css_classes << ' issue-behind-schedule' if issue.behind_schedule?
376
          css_classes << ' icon icon-issue' unless Setting.gravatar_enabled? && issue.assigned_to
377
          s = "".html_safe
378
          if issue.assigned_to.present?
379
            assigned_string = l(:field_assigned_to) + ": " + issue.assigned_to.name
380
            s << view.avatar(issue.assigned_to,
381
                             :class => 'gravatar icon-gravatar',
382
                             :size => 10,
383
                             :title => assigned_string).to_s.html_safe
384
          end
385
          s << view.link_to_issue(issue).html_safe
386
          subject = view.content_tag(:span, s, :class => css_classes).html_safe
387
          html_subject(options, subject, :css => "issue-subject",
388
                       :title => issue.subject) + "\n"
486
          if !grouped? || @detailed_groups
487
            css_classes = ''
488
            css_classes << ' issue-overdue' if issue.overdue?
489
            css_classes << ' issue-behind-schedule' if issue.behind_schedule?
490
            css_classes << ' icon icon-issue' unless Setting.gravatar_enabled? && issue.assigned_to
491
            s = "".html_safe
492
            if issue.assigned_to.present?
493
              assigned_string = l(:field_assigned_to) + ": " + issue.assigned_to.name
494
              s << view.avatar(issue.assigned_to,
495
                               :class => 'gravatar icon-gravatar',
496
                               :size => 10,
497
                               :title => assigned_string).to_s.html_safe
498
            end
499
            s << view.link_to_issue(issue).html_safe
500
            subject = view.content_tag(:span, s, :class => css_classes).html_safe
501
            html_subject(options, subject, :css => "issue-subject",
502
                         :title => issue.subject) + "\n"
503
          end
389 504
        when :image
390
          image_subject(options, issue.subject)
505
          if !grouped? || @detailed_groups
506
            image_subject(options, issue.subject)
507
          end
391 508
        when :pdf
392 509
          pdf_new_page?(options)
393
          pdf_subject(options, issue.subject)
510
          pdf_subject(options, (grouped? || @detailed_groups) ? ' ' : issue.subject)
394 511
        end
395 512
        unless issue.leaf?
396 513
          @issue_ancestors << issue
......
403 520
        # Skip issues that don't have a due_before (due_date or version's due_date)
404 521
        if issue.is_a?(Issue) && issue.due_before
405 522
          coords = coordinates(issue.start_date, issue.due_before, issue.done_ratio, options[:zoom])
406
          label = "#{issue.status.name} #{issue.done_ratio}%"
523
          label = " "
524
          if !grouped? || @detailed_groups
525
            label = "#{issue.status.name} #{issue.done_ratio}%"
526
          end
407 527
          case options[:format]
408 528
          when :html
409 529
            html_task(options, coords,
......
742 862
        [min, max]
743 863
      end
744 864

  
865
      def grouped?
866
        @query.grouped?
867
      end
868

  
869
      def new_group?(x, y)
870
        if grouped?
871
          value_x = @query.group_by_column.value(x)
872
          value_y = @query.group_by_column.value(y)
873

  
874
          value_x != value_y
875
        else
876
          true
877
        end
878
      end
879

  
880
      def group_name(issue)
881
        if grouped?
882
          result = @query.group_by_column.value(issue)
883
          if result == nil || result == ''
884
            'None'
885
          else
886
            result.to_s
887
          end
888
        else
889
          issue.subject
890
        end
891
      end
892

  
745 893
      def pdf_new_page?(options)
746 894
        if options[:top] > 180
747 895
          options[:pdf].Line(15, options[:top], PDF::TotalWidth, options[:top])
(1-1/2)