Project

General

Profile

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

Patch to add grouping to Gantt diagram (v2) - Tobias Droste, 2013-01-18 00:13

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
3 3

  
4 4
<%= form_tag({:controller => 'gantts', :action => 'show',
5 5
             :project_id => @project, :month => params[:month],
6
             :year => params[:year], :months => params[:months]},
6
                          :year => params[:year], :months => params[:months],
7
             :not_detailed_groups => params[:not_detailed_groups],
8
             :version_groups => params[:version_groups]},
7 9
             :method => :get, :id => 'query_form') do %>
8 10
<%= hidden_field_tag 'set_filter', '1' %>
9 11
<fieldset id="filters" class="collapsible <%= @query.new_record? ? "" : "collapsed" %>">
......
13 15
  </div>
14 16
</fieldset>
15 17

  
18
<fieldset class="collapsible collapsed" style="display: none;">
19

  
20
  <legend onclick="toggleFieldset(this);"><%= l(:label_options) %></legend>
21

  
22
  <div style="display: none;">
23

  
24
    <table>
25

  
26
      <tr>
27

  
28
        <td><%= l(:field_column_names) %></td>
29

  
30
        <td><%= render :partial => 'queries/columns', :locals => {:query => @query} %></td>
31

  
32
      </tr>
33

  
34
    </table>
35

  
36
  </div>
37

  
38
</fieldset>
39
<fieldset class="collapsible collapsed">
40
  <legend onclick="toggleFieldset(this);"><%= l(:label_options) %></legend>
41
  <div style="display: none;">
42
    <table>
43
      <tr>
44
        <td><label for='group_by'><%= l(:field_group_by) %></label></td>
45
        <td><%= select_tag('group_by',
46
                           options_for_select(
47
                             [[]] + @query.groupable_columns.collect {|c| [c.caption, c.name.to_s]},
48
                             @query.group_by)
49
                   ) %></td>      </tr>
50
      <tr>
51
        <td><%= l(:button_show) %></td>
52
        <td><%= content_tag('label', check_box_tag('not_detailed_groups', true, !@gantt.detailed_groups) + l(:label_hide_group_details), :class => 'inline') %></td>
53
      </tr>
54
      <tr>
55
        <td></td>
56
        <td><%= content_tag('label', check_box_tag('version_groups', true, @gantt.version_groups) + l(:label_keep_version_groups), :class => 'inline') %></td>
57
      </tr>
58
    </table>
59
  </div>
60
</fieldset>
61

  
16 62
<p class="contextual">
17 63
  <%= gantt_zoom_link(@gantt, :in) %>
18 64
  <%= gantt_zoom_link(@gantt, :out) %>
......
29 75
                     :class => 'icon icon-checked' %>
30 76
<%= link_to l(:button_clear), { :project_id => @project, :set_filter => 1 },
31 77
            :class => 'icon icon-reload' %>
78
<% if @query.new_record? && User.current.allowed_to?(:save_queries, @project, :global => true) %>
79
    <%= link_to_function l(:button_save),
80
            "$('query_form').action='#{ @project ? new_project_query_path(@project) : new_query_path }'; submit_query_form('query_form')",
81
            :class => 'icon icon-save' %>
82
<% end %>
32 83
</p>
33 84
<% end %>
34 85

  
config/locales/de.yml
585 585
  label_per_page: Pro Seite
586 586
  label_calendar: Kalender
587 587
  label_months_from: Monate ab
588
  label_hide_group_details: Verstecke Gruppendetail
589
  label_keep_version_groups: Behalte Versiongruppen
588 590
  label_gantt: Gantt-Diagramm
589 591
  label_internal: Intern
590 592
  label_last_changes: "%{count} letzte Änderungen"
config/locales/en-GB.yml
591 591
  label_per_page: Per page
592 592
  label_calendar: Calendar
593 593
  label_months_from: months from
594
  label_hide_group_details: Hide group details
595
  label_keep_version_groups: Keep version groups
594 596
  label_gantt: Gantt
595 597
  label_internal: Internal
596 598
  label_last_changes: "last %{count} changes"
config/locales/en.yml
632 632
  label_per_page: Per page
633 633
  label_calendar: Calendar
634 634
  label_months_from: months from
635
  label_hide_group_details: Hide group details
636
  label_keep_version_groups: Keep version groups
635 637
  label_gantt: Gantt
636 638
  label_internal: Internal
637 639
  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
......
57 58
        @zoom = (zoom > 0 && zoom < 5) ? zoom : 2
58 59
        months = (options[:months] || User.current.pref[:gantt_months]).to_i
59 60
        @months = (months > 0 && months < 25) ? months : 6
61
        detailed_groups = User.current.pref[:gantt_detailed_groups]
62
        if options[:set_filter] == '1'
63
          detailed_groups = (options[:not_detailed_groups].to_s == 'true') ? false : true
64
        end
65
        @detailed_groups = (detailed_groups.to_s == 'true') ? true : false
66
        version_groups = User.current.pref[:gantt_version_groups].to_s
67
        if options[:set_filter] == '1'
68
          version_groups = options[:version_groups].to_s
69
        end
70
        @version_groups = (version_groups == 'true') ? true : false
60 71
        # Save gantt parameters as user preference (zoom and months count)
61 72
        if (User.current.logged? && (@zoom != User.current.pref[:gantt_zoom] ||
62
              @months != User.current.pref[:gantt_months]))
73
              @months != User.current.pref[:gantt_months]) ||
74
              @detailed_groups != User.current.pref[:gantt_detailed_groups] ||
75
              @version_groups != User.current.pref[:gantt_version_groups])
63 76
          User.current.pref[:gantt_zoom], User.current.pref[:gantt_months] = @zoom, @months
77
          User.current.pref[:gantt_detailed_groups] = @detailed_groups
78
          User.current.pref[:gantt_version_groups] = @version_groups
64 79
          User.current.preference.save
65 80
        end
66 81
        @date_from = Date.civil(@year_from, @month_from, 1)
......
83 98

  
84 99
      def params
85 100
        common_params.merge({:zoom => zoom, :year => year_from,
86
                             :month => month_from, :months => months})
101
                             :month => month_from, :months => months,
102
                             :not_detailed_groups => !detailed_groups,
103
                             :version_groups => version_groups})
87 104
      end
88 105

  
89 106
      def params_previous
90 107
        common_params.merge({:year => (date_from << months).year,
91 108
                             :month => (date_from << months).month,
92
                             :zoom => zoom, :months => months})
109
                             :zoom => zoom, :months => months,
110
                             :not_detailed_groups => !detailed_groups,
111
                             :version_groups => version_groups})
93 112
      end
94 113

  
95 114
      def params_next
96 115
        common_params.merge({:year => (date_from >> months).year,
97 116
                             :month => (date_from >> months).month,
98
                             :zoom => zoom, :months => months})
117
                             :zoom => zoom, :months => months,
118
                             :not_detailed_groups => !detailed_groups,
119
                             :version_groups => version_groups})
99 120
      end
100 121

  
101 122
      # Returns the number of rows that will be rendered on the Gantt chart
......
168 189
        project_issues(project).select {|issue| issue.fixed_version == version}
169 190
      end
170 191

  
192
      # Returns the issues that belong to +project+ and are grouped by +group+
193
      def group_issues!(group, issues)
194
        result = issues.take_while {|issue| group_name(issue) == group}
195
        issues.reject! {|issue| group_name(issue) == group}
196
        result
197
      end
198

  
171 199
      def render(options={})
172 200
        options = {:top => 0, :top_increment => 20,
173 201
                   :indent_increment => 20, :render => :subject,
......
193 221
        options[:indent] += options[:indent_increment]
194 222
        @number_of_rows += 1
195 223
        return if abort?
196
        issues = project_issues(project).select {|i| i.fixed_version.nil?}
197
        sort_issues!(issues)
198
        if issues
224
        issues = project_issues(project).select {|i| i.fixed_version.nil? || (grouped? && !@version_groups) }
225
        if issues && grouped?
226
          render_groups(issues, options)
227
          return if abort?
228
        else
229
          sort_issues!(issues)
199 230
          render_issues(issues, options)
200 231
          return if abort?
201 232
        end
......
207 238
        options[:indent] -= options[:indent_increment]
208 239
      end
209 240

  
210
      def render_issues(issues, options={})
211
        @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?
218
        end
219
        options[:indent] -= (options[:indent_increment] * @issue_ancestors.size)
220
      end
221

  
222 241
      def render_version(project, version, options={})
223 242
        # Version header
224 243
        subject_for_version(version, options) unless options[:only] == :lines
225 244
        line_for_version(version, options) unless options[:only] == :subjects
226 245
        options[:top] += options[:top_increment]
246
        options[:indent] += options[:indent_increment]
227 247
        @number_of_rows += 1
228
        return if abort?
248
        return if abort? || (grouped? && !@version_groups)
229 249
        issues = version_issues(project, version)
230
        if issues
250
        if issues && grouped?
251
          render_groups(issues, options)
252
          return if abort?
253
        else
231 254
          sort_issues!(issues)
232
          # Indent issues
233
          options[:indent] += options[:indent_increment]
234 255
          render_issues(issues, options)
235
          options[:indent] -= options[:indent_increment]
256
          return if abort?
257
        end
258
        # Remove indent to hit the next sibling
259
        options[:indent] -= options[:indent_increment]
260
      end
261

  
262
      def render_groups(issues, options={})
263
        while !issues.empty?
264
          # Group header
265
          group = group_name(issues[0])
266
          subject_for_group(group, options) unless options[:only] == :lines
267
          options[:top] += options[:top_increment]
268
          @number_of_rows += 1
269
          break if abort?
270
          gr_issues = group_issues!(group, issues)
271
          if gr_issues
272
            sort_issues!(gr_issues)
273
            # Indent issues
274
            options[:indent] += options[:indent_increment]
275
            render_issues(gr_issues, options)
276
            options[:indent] -= options[:indent_increment]
277
          end
278
          break if abort?
279
        end
280
      end
281

  
282
      def render_issues(issues, options={})
283
        @issue_ancestors = []
284
        if !grouped? || @detailed_groups
285
          issues.each do |i|
286
            if i.due_before == nil
287
              i.due_date = i.start_date
288
            end
289
            subject_for_issue(i, options) unless options[:only] == :lines
290
            line_for_issue(i, options) unless options[:only] == :subjects
291
            options[:top] += options[:top_increment]
292
            @number_of_rows += 1
293
            break if abort?
294
          end
295
        else
296
          group_start = options[:top] - options[:top_increment]
297
          group_max_line = 0
298
          group_write_line = group_max_line
299
          dates_in_line = [[]]
300
          @number_of_rows += 1
301
          issues.each do |i|
302
            if i.leaf?
303
              if i.due_before == nil
304
                i.due_date = i.start_date
305
              end
306
              group_write_line = -1
307
              dates_in_line.each_with_index do |dates, line|
308
                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
309
                  group_write_line = line
310
                  break
311
                end
312
              end
313
              if group_write_line == -1
314
                group_max_line += 1
315
                group_write_line = group_max_line
316
                dates_in_line.push([])
317
                @number_of_rows += 1
318
              end
319
              options[:top] = group_start + (group_write_line * options[:top_increment])
320
              subject_for_issue(i, options) unless options[:only] == :lines
321
              line_for_issue(i, options) unless options[:only] == :subjects
322
              dates_in_line[group_write_line].push([i.start_date, i.due_before])
323
            end
324
            break if abort?
325
          end
326
          options[:top] = group_start + ((group_max_line+1) * options[:top_increment])
236 327
        end
328
        options[:indent] -= (options[:indent_increment] * @issue_ancestors.size)
237 329
      end
238 330

  
239 331
      def render_end(options={})
......
262 354
      end
263 355

  
264 356
      def line_for_project(project, options)
265
        # Skip versions that don't have a start_date or due date
357
        # Skip projects that don't have a start_date or due date
266 358
        if project.is_a?(Project) && project.start_date && project.due_date
267 359
          options[:zoom] ||= 1
268 360
          options[:g_width] ||= (self.date_to - self.date_from + 1) * options[:zoom]
......
282 374
        end
283 375
      end
284 376

  
377
      def subject_for_group(group, options)
378
        case options[:format]
379
        when :html
380
          html_class = ""
381
          html_class << 'icon icon-package '
382
          s = group.html_safe
383
          subject = view.content_tag(:span, s,
384
                                     :class => html_class).html_safe
385
          html_subject(options, subject, :css => "version-name")
386
        when :image
387
          image_subject(options, group)
388
        when :pdf
389
          pdf_new_page?(options)
390
          pdf_subject(options, group)
391
        end
392
      end
393

  
285 394
      def subject_for_version(version, options)
286 395
        case options[:format]
287 396
        when :html
......
332 441
        end
333 442
        output = case options[:format]
334 443
        when :html
335
          css_classes = ''
336
          css_classes << ' issue-overdue' if issue.overdue?
337
          css_classes << ' issue-behind-schedule' if issue.behind_schedule?
338
          css_classes << ' icon icon-issue' unless Setting.gravatar_enabled? && issue.assigned_to
339
          s = "".html_safe
340
          if issue.assigned_to.present?
341
            assigned_string = l(:field_assigned_to) + ": " + issue.assigned_to.name
342
            s << view.avatar(issue.assigned_to,
343
                             :class => 'gravatar icon-gravatar',
344
                             :size => 10,
345
                             :title => assigned_string).to_s.html_safe
346
          end
347
          s << view.link_to_issue(issue).html_safe
348
          subject = view.content_tag(:span, s, :class => css_classes).html_safe
349
          html_subject(options, subject, :css => "issue-subject",
350
                       :title => issue.subject) + "\n"
444
          if !grouped? || @detailed_groups
445
            css_classes = ''
446
            css_classes << ' issue-overdue' if issue.overdue?
447
            css_classes << ' issue-behind-schedule' if issue.behind_schedule?
448
            css_classes << ' icon icon-issue' unless Setting.gravatar_enabled? && issue.assigned_to
449
            s = "".html_safe
450
            if issue.assigned_to.present?
451
              assigned_string = l(:field_assigned_to) + ": " + issue.assigned_to.name
452
              s << view.avatar(issue.assigned_to,
453
                               :class => 'gravatar icon-gravatar',
454
                               :size => 10,
455
                               :title => assigned_string).to_s.html_safe
456
            end
457
            s << view.link_to_issue(issue).html_safe
458
            subject = view.content_tag(:span, s, :class => css_classes).html_safe
459
            html_subject(options, subject, :css => "issue-subject",
460
                         :title => issue.subject) + "\n"
461
          end
351 462
        when :image
352
          image_subject(options, issue.subject)
463
          if !grouped? || @detailed_groups
464
            image_subject(options, issue.subject)
465
          end
353 466
        when :pdf
354 467
          pdf_new_page?(options)
355
          pdf_subject(options, issue.subject)
468
          pdf_subject(options, (grouped? || @detailed_groups) ? ' ' : issue.subject)
356 469
        end
357 470
        unless issue.leaf?
358 471
          @issue_ancestors << issue
......
365 478
        # Skip issues that don't have a due_before (due_date or version's due_date)
366 479
        if issue.is_a?(Issue) && issue.due_before
367 480
          coords = coordinates(issue.start_date, issue.due_before, issue.done_ratio, options[:zoom])
368
          label = "#{issue.status.name} #{issue.done_ratio}%"
481
          label = " "
482
          if !grouped? || @detailed_groups
483
            label = "#{issue.status.name} #{issue.done_ratio}%"
484
          end
369 485
          case options[:format]
370 486
          when :html
371 487
            html_task(options, coords,
......
666 782
        end
667 783
      end
668 784

  
785
      def grouped?
786
        @query.grouped?
787
      end
788

  
789
      def new_group?(x, y)
790
        if grouped?
791
          value_x = @query.group_by_column.value(x)
792
          value_y = @query.group_by_column.value(y)
793
          value_x != value_y
794
        else
795
          true
796
        end
797
      end
798

  
799
      def group_name(issue)
800
        if grouped?
801
          result = @query.group_by_column.value(issue)
802
          if result == nil || result == ''
803
            'None'
804
          else
805
            result.to_s
806
          end
807
        else
808
          issue.subject
809
        end
810
      end
811

  
669 812
      def pdf_new_page?(options)
670 813
        if options[:top] > 180
671 814
          options[:pdf].Line(15, options[:top], PDF::TotalWidth, options[:top])
(2-2/2)