trunk-r10066.diff

Toshi MARUYAMA, 2012-07-21 17:36

Download (43 KB)

View differences:

app/helpers/application_helper.rb
1051 1051
    javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
1052 1052
  end
1053 1053

  
1054
  def gantt_calendar_for(field_id)
1055
    include_calendar_headers_tags
1056
    image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
1057
    javascript_tag("Calendar.setup({inputField : '#{field_id}', electric : false, ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
1058
  end
1059

  
1054 1060
  def include_calendar_headers_tags
1055 1061
    unless @calendar_headers_tags_included
1056 1062
      @calendar_headers_tags_included = true
lib/redmine/helpers/gantt.rb
65 65
        @date_to = (@date_from >> @months) - 1
66 66
        @subjects = ''
67 67
        @lines = ''
68
        @calendars = ''
68 69
        @number_of_rows = nil
69 70
        @issue_ancestors = []
70 71
        @truncated = false
......
121 122
        @lines
122 123
      end
123 124

  
125
      # Renders the calendars of the Gantt chart, the right side
126
      def calendars(options={})
127
        render(options.merge(:only => :calendars)) unless @calendars_rendered
128
        @calendars
129
      end
130

  
124 131
      # Returns issues that will be rendered
125 132
      def issues
126 133
        @issues ||= @query.issues(
......
165 172
      def render(options={})
166 173
        options = {:top => 0, :top_increment => 20, :indent_increment => 20, :render => :subject, :format => :html}.merge(options)
167 174
        indent = options[:indent] || 4
168
        @subjects = '' unless options[:only] == :lines
169
        @lines = '' unless options[:only] == :subjects
175
        if options[:format] == :html
176
          @subjects = '' unless options[:only] == :lines && options[:only] == :calendars
177
          @lines = '' unless options[:only] == :subjects && options[:only] == :calendars
178
          @calendars = '' unless options[:only] == :lines && options[:only] == :subjects
179
        else
180
          @subjects = '' unless options[:only] == :lines
181
          @lines = '' unless options[:only] == :subjects
182
        end
170 183
        @number_of_rows = 0
171 184
        Project.project_tree(projects) do |project, level|
172 185
          options[:indent] = indent + level * options[:indent_increment]
173 186
          render_project(project, options)
174 187
          break if abort?
175 188
        end
176
        @subjects_rendered = true unless options[:only] == :lines
177
        @lines_rendered = true unless options[:only] == :subjects
189
        if options[:format] == :html
190
          @subjects_rendered = true unless options[:only] == :lines && options[:only] == :calendars
191
          @lines_rendered = true unless options[:only] == :subjects && options[:only] == :calendars
192
          @calendars_rendered = true unless options[:only] == :lines && options[:only] == :subjects
193
        else
194
          @subjects_rendered = true unless options[:only] == :lines
195
          @lines_rendered = true unless options[:only] == :subjects
196
        end
178 197
        render_end(options)
179 198
      end
180 199

  
181 200
      def render_project(project, options={})
182
        subject_for_project(project, options) unless options[:only] == :lines
183
        line_for_project(project, options) unless options[:only] == :subjects
201
        if options[:format] == :html
202
          subject_for_project(project, options) unless options[:only] == :lines && options[:only] == :calendars
203
          line_for_project(project, options) unless options[:only] == :subjects && options[:only] == :calendars
204
          calendar_for_project(project, options) unless options[:only] == :lines && options[:only] == :subjects
205
        else
206
          subject_for_project(project, options) unless options[:only] == :lines
207
          line_for_project(project, options) unless options[:only] == :subjects
208
        end
184 209
        options[:top] += options[:top_increment]
185 210
        options[:indent] += options[:indent_increment]
186 211
        @number_of_rows += 1
......
202 227
      def render_issues(issues, options={})
203 228
        @issue_ancestors = []
204 229
        issues.each do |i|
205
          subject_for_issue(i, options) unless options[:only] == :lines
206
          line_for_issue(i, options) unless options[:only] == :subjects
230
          if options[:format] == :html
231
            subject_for_issue(i, options) unless options[:only] == :lines && options[:only] == :calendars
232
            line_for_issue(i, options) unless options[:only] == :subjects && options[:only] == :calendars
233
            calendar_for_issue(i, options) unless options[:only] == :lines && options[:only] == :subjects
234
          else
235
            subject_for_issue(i, options) unless options[:only] == :lines
236
            line_for_issue(i, options) unless options[:only] == :subjects
237
          end
207 238
          options[:top] += options[:top_increment]
208 239
          @number_of_rows += 1
209 240
          break if abort?
......
213 244

  
214 245
      def render_version(project, version, options={})
215 246
        # Version header
216
        subject_for_version(version, options) unless options[:only] == :lines
217
        line_for_version(version, options) unless options[:only] == :subjects
247
        if options[:format] == :html
248
          subject_for_version(version, options) unless options[:only] == :lines && options[:only] == :calendars
249
          line_for_version(version, options) unless options[:only] == :subjects && options[:only] == :calendars
250
          calendar_for_version(version, options) unless options[:only] == :lines && options[:only] == :subjects
251
        else
252
          subject_for_version(version, options) unless options[:only] == :lines
253
          line_for_version(version, options) unless options[:only] == :subjects
254
        end
218 255
        options[:top] += options[:top_increment]
219 256
        @number_of_rows += 1
220 257
        return if abort?
......
259 296
          label = h(project)
260 297
          case options[:format]
261 298
          when :html
262
            html_task(options, coords, :css => "project task", :label => label, :markers => true)
299
            html_task(options, coords, :css => "project task", :label => label, :markers => true, :id => project.id, :kind => "p")
263 300
          when :image
264 301
            image_task(options, coords, :label => label, :markers => true, :height => 3)
265 302
          when :pdf
......
296 333
          label = h("#{version.project} -") + label unless @project && @project == version.project
297 334
          case options[:format]
298 335
          when :html
299
            html_task(options, coords, :css => "version task", :label => label, :markers => true)
336
            html_task(options, coords, :css => "version task", :label => label, :markers => true, :id => version.id, :kind => "v")
300 337
          when :image
301 338
            image_task(options, coords, :label => label, :markers => true, :height => 3)
302 339
          when :pdf
......
346 383
        if issue.is_a?(Issue) && issue.due_before
347 384
          coords = coordinates(issue.start_date, issue.due_before, issue.done_ratio, options[:zoom])
348 385
          label = "#{ issue.status.name } #{ issue.done_ratio }%"
386
          if !issue.due_date && issue.fixed_version
387
            if options[:format] == :html
388
              label += "-&nbsp;<strong>#{h(issue.fixed_version.name)}</strong>"
389
            else
390
              label += "-#{h(issue.fixed_version.name)}"
391
            end
392
          end
349 393
          case options[:format]
350 394
          when :html
351
            html_task(options, coords, :css => "task " + (issue.leaf? ? 'leaf' : 'parent'), :label => label, :issue => issue, :markers => !issue.leaf?)
395
            html_task(options, coords, :css => "task " + (issue.leaf? ? 'leaf' : 'parent'),
396
                     :label => label, :issue => issue, :markers => !issue.leaf?, :id => issue.id, :kind => "i")
352 397
          when :image
353 398
            image_task(options, coords, :label => label)
354 399
          when :pdf
......
568 613
        pdf.Output
569 614
      end
570 615

  
616
      def edit(pms)
617
        id = pms[:id]
618
        kind = id.slice!(0).chr
619
        begin
620
          case kind
621
          when 'i'
622
            @issue = Issue.find(pms[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
623
          when 'p'
624
            @issue = Project.find(pms[:id])
625
          when 'v'
626
            @issue = Version.find(pms[:id], :include => [:project])
627
          end
628
        rescue ActiveRecord::RecordNotFound
629
          return "issue not found : #{pms[:id]}", 400
630
        end
631

  
632
        if !@issue.start_date || !@issue.due_before
633
          #render :text=>l(:notice_locking_conflict), :status=>400
634
          return l(:notice_locking_conflict), 400
635
        end
636
        @issue.init_journal(User.current)
637
        date_from = Date.parse(pms[:date_from])
638
        old_start_date = @issue.start_date
639
        o = get_issue_position(@issue, pms[:zoom])
640
        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]}".html_safe
641

  
642
        if pms[:day]
643
          #bar moved
644
          day = pms[:day].to_i
645
          duration = @issue.due_before - @issue.start_date
646
          @issue.start_date = date_from + day
647
          @issue.due_date = @issue.start_date + duration.to_i if @issue.due_date
648
        elsif pms[:start_date]
649
          #start date changed
650
          start_date = Date.parse(pms[:start_date])
651
          if @issue.start_date == start_date
652
            #render :text=>""
653
            return "", 200 #nothing has changed
654
          end
655
          @issue.start_date = start_date
656
          @issue.due_date = start_date if @issue.due_date && start_date > @issue.due_date
657
        elsif pms[:due_date]
658
          #due date changed
659
          due_date = Date.parse(pms[:due_date])
660
          if @issue.due_date == due_date
661
            #render :text=>""
662
            return "", 200 #nothing has changed
663
          end
664
          @issue.due_date = due_date
665
          @issue.start_date = due_date if due_date < @issue.start_date
666
        end
667
        fv = @issue.fixed_version
668
        if fv && fv.effective_date && !@issue.due_date && fv.effective_date < @issue.start_date
669
          @issue.start_date = old_start_date
670
        end
671

  
672
        begin
673
          @issue.save!
674
          o = get_issue_position(@issue, pms[:zoom])
675
          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]}".html_safe
676

  
677
          prj_map = {}
678
          text = set_project_data(@issue.project, pms[:zoom], text, prj_map)
679
          version_map = {}
680
          text = set_version_data(@issue.fixed_version, pms[:zoom], text, version_map)
681

  
682
          #check dependencies
683
          issues = @issue.all_precedes_issues
684
          issues.each do |i|
685
            o = get_issue_position(i, pms[:zoom])
686
            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]}".html_safe
687
            text = set_project_data(i.project, pms[:zoom], text, prj_map)
688
            text = set_version_data(i.fixed_version, pms[:zoom], text, version_map)
689
          end
690

  
691
          #check parent
692
          is = @issue
693
          while
694
            pid = is.parent_issue_id
695
            break if !pid
696
            i = Issue.find(pid)
697
            o = get_issue_position(i, pms[:zoom])
698
            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]},#{o[4]},#{o[5]}".html_safe
699
            text = set_project_data(i.project, pms[:zoom], text, prj_map)
700
            text = set_version_data(i.fixed_version, pms[:zoom], text, version_map)
701
            is = i
702
          end
703
          #render :text=>text
704
          return text, 200
705
        rescue => e
706
          #render :text=>@issue.errors.full_messages.join("\n") + "|" + text_for_revert  , :status=>400
707
          if @issue.errors.full_messages.to_s == ""
708
            return e.to_s + "\n" + [$!,$@.join("\n")].join("\n") + "\n" + @issue.errors.full_messages.join("\n") + "|" + text_for_revert, 400
709
          else
710
            return @issue.errors.full_messages.join("\n") + "|" + text_for_revert, 400
711
          end
712
        end
713
      end
714

  
571 715
      private
572 716

  
573 717
      def coordinates(start_date, end_date, progress, zoom=nil)
......
681 825
        output = ''
682 826
        # Renders the task bar, with progress and late
683 827
        if coords[:bar_start] && coords[:bar_end]
684
          output << "<div style='top:#{ params[:top] }px;left:#{ coords[:bar_start] }px;width:#{ coords[:bar_end] - coords[:bar_start] - 2}px;' class='#{options[:css]} task_todo'>&nbsp;</div>".html_safe
828
          i_width = coords[:bar_end] - coords[:bar_start] - 2
829
          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'" : ""}>".html_safe
830
          output << "<div id='task_todo_#{options[:kind]}#{options[:id]}' style='float:left:0px; width:#{ i_width}px;' class='#{options[:css]} task_todo'>&nbsp;</div>".html_safe
685 831
          if coords[:bar_late_end]
686
            output << "<div style='top:#{ params[:top] }px;left:#{ coords[:bar_start] }px;width:#{ coords[:bar_late_end] - coords[:bar_start] - 2}px;' class='#{options[:css]} task_late'>&nbsp;</div>".html_safe
832
            l_width = coords[:bar_late_end] - coords[:bar_start] - 2
833
            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>".html_safe
834
          else
835
            output << "<div id='task_late_#{options[:kind]}#{options[:id]}' style='float:left:0px; width:0px;' class='#{ options[:css] + " task_none"}'>&nbsp;</div>".html_safe
687 836
          end
688 837
          if coords[:bar_progress_end]
689
            output << "<div style='top:#{ params[:top] }px;left:#{ coords[:bar_start] }px;width:#{ coords[:bar_progress_end] - coords[:bar_start] - 2}px;' class='#{options[:css]} task_done'>&nbsp;</div>".html_safe
838
            d_width = coords[:bar_progress_end] - coords[:bar_start] - 2
839
            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>".html_safe
840
          else
841
            output << "<div id='task_done_#{options[:kind]}#{options[:id]}' style='float:left:0px; width:0px;' class='#{ options[:css] + " task_none"}'>&nbsp;</div>".html_safe
690 842
          end
843
          output << "</div>".html_safe
844
        else
845
          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'" : ""}>".html_safe
846
          output << "<div id='task_todo_#{options[:kind]}#{options[:id]}' style='float:left:0px; width:0px;' class='#{ options[:css]} task_todo'>&nbsp;</div>".html_safe
847
          output << "<div id='task_late_#{options[:kind]}#{options[:id]}' style='float:left:0px; width:0px;' class='#{ options[:css] + " task_none"}'>&nbsp;</div>".html_safe
848
          output << "<div id='task_done_#{options[:kind]}#{options[:id]}' style='float:left:0px; width:0px;' class='#{ options[:css] + " task_none"}'>&nbsp;</div>".html_safe
849
          output << "</div>".html_safe
691 850
        end
692 851
        # Renders the markers
693 852
        if options[:markers]
694 853
          if coords[:start]
695
            output << "<div style='top:#{ params[:top] }px;left:#{ coords[:start] }px;width:15px;' class='#{options[:css]} marker starting'>&nbsp;</div>".html_safe
854
            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>".html_safe
696 855
          end
697 856
          if coords[:end]
698
            output << "<div style='top:#{ params[:top] }px;left:#{ coords[:end] + params[:zoom] }px;width:15px;' class='#{options[:css]} marker ending'>&nbsp;</div>".html_safe
857
            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>".html_safe
699 858
          end
859
        else
860
          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);}')         #pend
700 861
        end
701 862
        # Renders the label on the right
702 863
        if options[:label]
703
          output << "<div style='top:#{ params[:top] }px;left:#{ (coords[:bar_end] || 0) + 8 }px;' class='#{options[:css]} label'>".html_safe
864
          output << "<div id='label_#{options[:kind]}#{options[:id]}' style='top:#{ params[:top] }px;left:#{ (coords[:bar_end] || 0) + 8 }px;' class='#{options[:css]} label'>".html_safe
704 865
          output << options[:label]
705 866
          output << "</div>".html_safe
706 867
        end
707 868
        # Renders the tooltip
708 869
        if options[:issue] && coords[:bar_start] && coords[:bar_end]
709
          output << "<div class='tooltip' style='position: absolute;top:#{ params[:top] }px;left:#{ coords[:bar_start] }px;width:#{ coords[:bar_end] - coords[:bar_start] }px;height:12px;'>".html_safe
870
          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;'>".html_safe
710 871
          output << '<span class="tip">'.html_safe
711 872
          output << view.render_issue_tooltip(options[:issue]).html_safe
712 873
          output << "</span></div>".html_safe
......
794 955
          params[:image].text(params[:subject_width] + (coords[:bar_end] || 0) + 5,params[:top] + 1, options[:label])
795 956
        end
796 957
      end
958

  
959
      ##  for edit gantt
960
      def set_project_data(prj, zoom, text, prj_map = {})
961
        if !prj
962
          return text
963
        end
964
        if !prj_map[prj.id]
965
          o = get_project_position(prj, zoom)
966
          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]}"
967
          prj_map[prj.id] = prj
968
        end
969
        text = set_project_data(prj.parent, zoom, text, prj_map)
970
      end
971

  
972
      def set_version_data(version, zoom, text, version_map = {})
973
        if !version
974
          return text
975
        end
976
        if !version_map[version.id]
977
          o = get_version_position(version, zoom)
978
          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]}"
979
          version_map[version.id] = version
980
        end
981
        return text
982
      end
983

  
984
      def get_pos(coords)
985
        i_left = 0
986
        i_width = 0
987
        l_width = 0
988
        d_width = 0
989
        if coords[:bar_start]
990
          i_left = coords[:bar_start]
991
          if coords[:bar_end]
992
            i_width = coords[:bar_end] - coords[:bar_start] - 2
993
            i_width = 0 if i_width < 0
994
          end
995
          if coords[:bar_late_end]
996
            l_width = coords[:bar_late_end] - coords[:bar_start] - 2
997
          end
998
          if coords[:bar_progress_end]
999
            d_width = coords[:bar_progress_end] - coords[:bar_start] - 2
1000
          end
1001
        end
1002
        return i_left, i_width, l_width, d_width
1003
      end
1004

  
1005
      def get_issue_position(issue, zoom_str)
1006
        z = zoom_str.to_i
1007
        zoom = 1
1008
        z.times { zoom = zoom * 2}
1009
        id = issue.due_before
1010
        if id && @date_to < id
1011
          id = @date_to
1012
        end
1013
        coords = coordinates(issue.start_date, id, issue.done_ratio, zoom)
1014

  
1015
        i_left, i_width, l_width, d_width = get_pos(coords)
1016
        if coords[:end]
1017
          return i_left, i_width, l_width, d_width, coords[:start], coords[:end] + zoom
1018
        else
1019
          return i_left, i_width, l_width, d_width, coords[:start], nil
1020
        end
1021
      end
1022

  
1023
      def get_project_position(project, zoom_str)
1024
        z = zoom_str.to_i
1025
        zoom = 1
1026
        z.times { zoom = zoom * 2}
1027
        pd = project.due_date
1028
        if pd && @date_to < pd
1029
          pd = @date_to
1030
        end
1031
        coords = coordinates(project.start_date, pd, nil, zoom)
1032
        i_left, i_width, l_width, d_width = get_pos(coords)
1033
        if coords[:end]
1034
          return i_left, i_width, l_width, d_width, coords[:start], coords[:end] + zoom
1035
        else
1036
          return i_left, i_width, l_width, d_width, coords[:start], nil
1037
        end
1038
      end
1039

  
1040
      def get_version_position(version, zoom_str)
1041
        z = zoom_str.to_i
1042
        zoom = 1
1043
        z.times { zoom = zoom * 2}
1044
        vd = version.due_date
1045
        if vd &&  @date_to < vd
1046
          vd = @date_to
1047
        end
1048
        coords = coordinates(version.start_date, vd, version.completed_pourcent, zoom)
1049
        i_left, i_width, l_width, d_width = get_pos(coords)
1050
        if coords[:end]
1051
          return i_left, i_width, l_width, d_width, coords[:start], coords[:end] + zoom
1052
        else
1053
          return i_left, i_width, l_width, d_width, coords[:start], nil
1054
        end
1055
      end
1056

  
1057
      def calendar_for_issue(issue, options)
1058
        # Skip issues that don't have a due_before (due_date or version's due_date)
1059
        if issue.is_a?(Issue) && issue.due_before
1060

  
1061
          case options[:format]
1062
          when :html
1063
            @calendars << "<div style='position: absolute;line-height:1.2em;height:16px;top:#{options[:top]}px;left:4px;overflow:hidden;width:180px;'>"
1064
            start_date = issue.start_date
1065
            if start_date
1066
              @calendars << "<div style='float: left; line-height: 1em; width: 90px;'>"
1067
              @calendars << "<span id='i#{issue.id}_start_date_str'>"
1068
              @calendars << format_date(start_date)
1069
              @calendars << "</span>"
1070
              @calendars << "<input type='hidden' size='12' id='i#{issue.id}_hidden_start_date' value='#{start_date}' />"
1071
              if issue.leaf?
1072
                @calendars << "<input type='hidden' size='12' id='i#{issue.id}_start_date' value='#{start_date}' />#{view.gantt_calendar_for('i' + issue.id.to_s + '_start_date')}"
1073
              else
1074
                @calendars << "<input type='hidden' size='12' id='i#{issue.id}_start_date' value='#{start_date}' />&nbsp;&nbsp;&nbsp;"
1075
              end
1076
              @calendars << observe_date_field("i#{issue.id}", 'start')
1077
              @calendars << "</div>"
1078
            end
1079
            due_date = issue.due_date
1080
            if due_date
1081
              @calendars << "<div style='float: right; line-height: 1em; width: 90px;'>"
1082
              @calendars << "<span id='i#{issue.id}_due_date_str'>"
1083
              @calendars << format_date(due_date)
1084
              @calendars << "</span>"
1085
              @calendars << "<input type='hidden' size='12' id='i#{issue.id}_hidden_due_date' value='#{due_date}' />"
1086
              if issue.leaf?
1087
                @calendars << "<input type='hidden' size='12' id='i#{issue.id}_due_date' value='#{due_date}' />#{view.gantt_calendar_for('i' + issue.id.to_s + '_due_date')}"
1088
              else
1089
                @calendars << "<input type='hidden' size='12' id='i#{issue.id}_due_date' value='#{due_date}' />"
1090
              end
1091
              @calendars << observe_date_field("i#{issue.id}", 'due')
1092
              @calendars << "</div>"
1093
            else
1094
              @calendars << "<div style='float: right; line-height: 1em; width: 90px;'>"
1095
              @calendars << "<span id='i#{issue.id}_due_date_str'>"
1096
              @calendars << "Not set"
1097
              @calendars << "</span>"
1098
              @calendars << "<input type='hidden' size='12' id='i#{issue.id}_hidden_due_date' value='#{start_date}' />"
1099
              if issue.leaf?
1100
                @calendars << "<input type='hidden' size='12' id='i#{issue.id}_due_date' value='#{start_date}' />#{view.gantt_calendar_for('i' + issue.id.to_s + '_due_date')}"
1101
              else
1102
                @calendars << "<input type='hidden' size='12' id='i#{issue.id}_due_date' value='#{start_date}' />"
1103
              end
1104
              @calendars << observe_date_field("i#{issue.id}", 'due')
1105
              @calendars << "</div>"            
1106
            end
1107
            
1108
            @calendars << "</div>"
1109
          when :image
1110
            #nop
1111
          when :pdf
1112
            #nop
1113
          end
1114
        else
1115
          ActiveRecord::Base.logger.debug "GanttHelper#line_for_issue was not given an issue with a due_before"
1116
          ''
1117
        end
1118
      end
1119

  
1120
      def calendar_for_version(version, options)
1121
        # Skip version that don't have a due_before (due_date or version's due_date)
1122
        if version.is_a?(Version) && version.start_date && version.due_date
1123

  
1124
          case options[:format]
1125
          when :html
1126
            @calendars << "<div style='position: absolute;line-height:1.2em;height:16px;top:#{options[:top]}px;left:4px;overflow:hidden;'>"
1127
            @calendars << "<span id='v#{version.id}_start_date_str'>"
1128
            @calendars << format_date(version.effective_date)
1129
            @calendars << "</span>"
1130
            @calendars << "</div>"
1131
          when :image
1132
            #nop
1133
          when :pdf
1134
            #nop
1135
          end
1136
        else
1137
          ActiveRecord::Base.logger.debug "GanttHelper#line_for_issue was not given an issue with a due_before"
1138
          ''
1139
        end
1140
      end
1141

  
1142
      def calendar_for_project(project, options)
1143
        case options[:format]
1144
        when :html
1145
          @calendars << "<div style='position: absolute;line-height:1.2em;height:16px;top:#{options[:top]}px;left:4px;overflow:hidden;width:180px;'>"
1146
          @calendars << "<div style='float:left;width:90px;'>"
1147
          @calendars << "<span id='p#{project.id}_start_date_str'>"
1148
          @calendars << format_date(project.start_date) if project.start_date
1149
          @calendars << "</span>"
1150
          @calendars << "</div>"
1151
          @calendars << "<div style='float:right;width:90px;'>"
1152
          @calendars << "<span id='p#{project.id}_due_date_str'>"
1153
          @calendars << format_date(project.due_date) if project.due_date
1154
          @calendars << "</span>"
1155
          @calendars << "</div>"
1156
          @calendars << "</div>"
1157
        when :image
1158
          # nop
1159
        when :pdf
1160
          # nop
1161
        end
1162
      end
1163

  
1164
      def observe_date_field(id, type)
1165
        output = ''
1166
        prj_id = ''
1167
        prj_id = @project.to_param if @project
1168
        output << "<script type='text/javascript'>\n"
1169
        output << "//<![CDATA[\n"
1170
        output << "new Form.Element.Observer('#{id}_#{type}_date', 0.25,\n"
1171
        output << "  function(element, value) {\n"
1172
        output << "    if (value == document.getElementById('#{id}_hidden_#{type}_date').value) {\n"
1173
        output << "      return ;\n"
1174
        output << "    }\n"
1175
        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)});"
1176
        output << "  })\n"
1177
        output << "//]]>\n"
1178
        output << "</script>"
1179
      end
797 1180
    end
798 1181
  end
799 1182
end
app/views/gantts/show.html.erb
1
<% include_calendar_headers_tags %>
1 2
<% @gantt.view = self %>
2 3
<h2><%= @query.new_record? ? l(:label_gantt) : h(@query.name) %></h2>
3 4

  
......
36 37
zoom = 1
37 38
@gantt.zoom.times { zoom = zoom * 2 }
38 39

  
39
subject_width = 330
40
subject_width = 280
40 41
header_heigth = 18
41 42

  
42 43
headers_height = header_heigth
......
61 62
g_height = [(20 * (@gantt.number_of_rows + 6)) + 150, 206].max
62 63
t_height = g_height + headers_height
63 64
%>
65
<input type="hidden" name="_date_from" id="i_date_from" value="<%= h(@gantt.date_from) %>" />
66
<input type="hidden" name="_date_to" id="i_date_to" value="<%= h(@gantt.date_to) %>" />
67
<input type="hidden" name="zm" id="i_zm" value="<%= zoom %>" />
68
<input type="hidden" name="pzm" id="i_pzm" value="<%= @gantt.zoom %>" />
69
<script type='text/javascript'>
70
  function issue_moved(elem) {
71
    var id_str = elem.id.substring(3, elem.id.length);
72
    var v_date_from = document.getElementById('i_date_from').getAttribute("value");
73
    var v_date_to = document.getElementById('i_date_to').getAttribute("value");
74
    var v_zm = document.getElementById('i_zm').getAttribute("value");
75
    var v_pzm = document.getElementById('i_pzm').getAttribute("value");
76
    var url_str = '<%=  url_for(:controller=>:gantts, :action => :edit_gantt) %>';
77
    url_str = url_str + "/" + id_str;
78
    var day = parseInt(elem.style.left)/parseInt(v_zm);
79
    new Ajax.Request(url_str, {asynchronous:true, evalScripts:true,
80
      parameters: 'day='+day+'&date_from='+v_date_from+'&date_to='+v_date_to+'&zoom='+v_pzm+"&project_id=<%= @project.to_param %>",
81
      onSuccess:function(obj) {
82
        change_dates(obj.responseText);
83
      },
84
      onFailure:function(obj) {
85
        handle_failure(obj.responseText);
86
      }
87
    });
88
  }
89

  
90
  function handle_failure(res_text) {
91
    var text = res_text.split('|');
92
    alert(text[0]);
93
    if (text.length == 1) {
94
      return;
95
    }
96
    change_dates(text[1]);//revert
97
  }
98

  
99
  function change_dates(issue_infos) {
100
    if (!issue_infos) {
101
      return;
102
    }
103
    var issue_list = issue_infos.split("|");
104
    for (i = 0; i < issue_list.length; i++) {
105
      change_date(issue_list[i]);
106
    }
107
  }
108

  
109
  function change_date(text) {
110
    if (!text) {
111
      return;
112
    }
113
    var issue_info = text.split("=");
114
    var elem_id = issue_info[0];
115
    var kind = elem_id.substring(0,1);
116
    var preClassName = "";
117
    if (kind == 'v') {
118
      preClassName = "version ";
119
    } else if (kind == 'p') {
120
      preClassName = "project ";
121
    }
122
    var vals = issue_info[1].split(',');
123
    var start_date_elem = document.getElementById(elem_id + '_start_date_str');
124
    if (!start_date_elem) {
125
      //target not exists
126
      return;
127
    }
128
    start_date_elem.innerHTML = vals[0];
129
    var tooltip_start_date_elem = document.getElementById('tooltip_start_date_' + elem_id);
130
    if (tooltip_start_date_elem) {
131
      tooltip_start_date_elem.innerHTML = vals[0];
132
    }
133
    var due_date_elem = document.getElementById(elem_id + '_due_date_str');
134
    if (due_date_elem) {
135
      due_date_elem.innerHTML = vals[2];
136
    }
137
    
138
    var tooltip_due_date_elem = document.getElementById('tooltip_due_date_' + elem_id);
139
    if (tooltip_due_date_elem) {
140
      tooltip_due_date_elem.innerHTML = vals[2];
141
    }
142
    
143
    var ev_elem = document.getElementById('ev_' + elem_id);
144
    if (ev_elem) {
145
      ev_elem.style.left = vals[4] + 'px';
146
      ev_elem.style.width = (parseInt(vals[5])+100)+'px';
147
    }
148
    var todo_elem = document.getElementById('task_todo_' + elem_id);
149
    if (todo_elem) {
150
      todo_elem.style.width = vals[5] + 'px';
151
    }
152
    
153
    var late_elem = document.getElementById('task_late_' + elem_id);
154
    if (late_elem) {
155
      var parentStr = "";
156
      if (late_elem.className.indexOf("parent") > 0) parentStr = "parent ";
157
      late_elem.style.width = vals[6] + 'px';
158
      if (vals[6] == '0') {
159
        late_elem.className = preClassName + 'task ' + parentStr + 'task_none';
160
      } else {
161
        late_elem.className = preClassName + 'task ' + parentStr + 'task_late';
162
      }
163
    }
164
    var done_elem = document.getElementById('task_done_' + elem_id);
165
    if (done_elem) {
166
      var parentStr = "";
167
      if (done_elem.className.indexOf("parent") > 0) parentStr = "parent ";
168
      done_elem.style.width = vals[7] + 'px';
169
      if (vals[7] == '0') {
170
        done_elem.className = preClassName + 'task ' + parentStr + 'task_none';
171
      } else {
172
        done_elem.className = preClassName + 'task ' + parentStr + 'task_done';
173
      }
174
    }
175
    var tooltip = document.getElementById("tt_" + elem_id);
176
    if (tooltip) {
177
      tooltip.style.left = ev_elem.style.left;
178
    }
179

  
180
    var label = document.getElementById("label_" + elem_id);
181
    if (label) {
182
      label.style.left = (parseInt(vals[4]) + parseInt(vals[5]) + 8) + 'px';
183
    }
184
    var marker_start = document.getElementById("marker_start_" + elem_id);
185
    if (marker_start && vals[8]) {
186
      marker_start.style.left = vals[8] + 'px';
187
    }
188
    var marker_end = document.getElementById("marker_end_" + elem_id);
189
    if (marker_end && vals[9]) {
190
      marker_end.style.left = vals[9] + 'px';
191
    }
192

  
193
    //change calendar date
194
    var elm1 = document.getElementById(elem_id+"_hidden_start_date");
195
    if (elm1) elm1.value = vals[1];
196
    var elm2 = document.getElementById(elem_id+"_start_date");
197
    if (elm2) elm2.value = vals[1];
198
    var elm3 = document.getElementById(elem_id+"_hidden_due_date");
199
    if (elm3) elm3.value = vals[3];
200
    var elm4 = document.getElementById(elem_id+"_due_date");
201
    if (elm4) elm4.value = vals[3];
202
  }
203
</script>
64 204

  
65 205
<% if @gantt.truncated %>
66 206
  <p class="warning"><%= l(:notice_gantt_chart_truncated, :max => @gantt.max_rows) %></p>
......
70 210
<tr>
71 211
<td style="width:<%= subject_width %>px; padding:0px;">
72 212

  
73
<div style="position:relative;height:<%= t_height + 24 %>px;width:<%= subject_width + 1 %>px;">
74
<div style="right:-2px;width:<%= subject_width %>px;height:<%= headers_height %>px;background: #eee;" class="gantt_hdr"></div>
75
<div style="right:-2px;width:<%= subject_width %>px;height:<%= t_height %>px;border-left: 1px solid #c0c0c0;overflow:hidden;" class="gantt_hdr"></div>
213
<div style="position:relative;height:<%= t_height + 24 %>px;width:<%= subject_width - 2 %>px;">
214
<div style="right:-2px;width:<%= subject_width - 2 %>px;height:<%= headers_height %>px;background: #eee;" class="gantt_hdr"></div>
215
<div style="right:-2px;width:<%= subject_width - 2 %>px;height:<%= t_height %>px;border-left: 1px solid #c0c0c0;overflow:hidden;" class="gantt_hdr"></div>
76 216

  
77 217
<div class="gantt_subjects">
78 218
<%= @gantt.subjects.html_safe %>
......
80 220

  
81 221
</div>
82 222
</td>
223

  
224
<td style="width:180px; padding:0px;">
225
<div style="position:relative;height:<%= t_height + 24 %>px;width:180px;">
226
<div style="width:181px;height:<%= headers_height %>px;background: #eee;" class="gantt_hdr"></div>
227
<div style="width:181px;height:<%= t_height %>px;overflow:hidden;" class="gantt_hdr"></div>
228
<div class="gantt_subjects">
229
  <%= @gantt.calendars.html_safe %>
230
</div>
231
</div>
232
</td>
83 233
<td>
84 234

  
85
<div style="position:relative;height:<%= t_height + 24 %>px;overflow:auto;">
235
<div style="position:relative;height:<%= t_height + 24 %>px;overflow:auto;" id="gantt-container">
86 236
<div style="width:<%= g_width - 1 %>px;height:<%= headers_height %>px;background: #eee;" class="gantt_hdr">&nbsp;</div>
87 237
<%
88 238
#
......
142 292
  left = 0
143 293
  height = g_height + header_heigth - 1
144 294
  wday = @gantt.date_from.cwday
295
  dt = @gantt.date_from
145 296
  (@gantt.date_to - @gantt.date_from + 1).to_i.times do
146 297
  width =  zoom - 1
147 298
  %>
148
  <div style="left:<%= left %>px;top:37px;width:<%= width %>px;height:<%= height %>px;font-size:0.7em;<%= "background:#f1f1f1;" if wday > 5 %>" class="gantt_hdr">
299
  <div style="left:<%= left %>px;top:37px;width:<%= width %>px;height:<%= height %>px;font-size:0.6em;<%= "background:#f1f1f1;" if wday > 5 %>" class="gantt_hdr">
300
    <% if @gantt.zoom == 4 %>
301
      <%= "#{dt.day}" %><br>
302
    <% end %>
149 303
  <%= day_name(wday).first %>
150 304
  </div>
151 305
  <%
152 306
  left = left + width + 1
153 307
  wday = wday + 1
308
  dt = dt + 1
154 309
  wday = 1 if wday > 7
155 310
  end
156 311
end %>
app/helpers/issues_helper.rb
51 51
    link_to_issue(issue) + "<br /><br />".html_safe +
52 52
      "<strong>#{@cached_label_project}</strong>: #{link_to_project(issue.project)}<br />".html_safe +
53 53
      "<strong>#{@cached_label_status}</strong>: #{h(issue.status.name)}<br />".html_safe +
54
      "<strong>#{@cached_label_start_date}</strong>: #{format_date(issue.start_date)}<br />".html_safe +
55
      "<strong>#{@cached_label_due_date}</strong>: #{format_date(issue.due_date)}<br />".html_safe +
54
      "<strong>#{@cached_label_start_date}</strong>: ".html_safe +
55
      "<span id='tooltip_start_date_i#{issue.id}'>#{format_date(issue.start_date)}</span><br />".html_safe +
56
      "<strong>#{@cached_label_due_date}</strong>: ".html_safe +
57
      "<span id='tooltip_due_date_i#{issue.id}'>#{format_date(issue.due_date)}</span><br />".html_safe +
56 58
      "<strong>#{@cached_label_assigned_to}</strong>: #{h(issue.assigned_to)}<br />".html_safe +
57 59
      "<strong>#{@cached_label_priority}</strong>: #{h(issue.priority.name)}".html_safe
58 60
  end
public/stylesheets/application.css
925 925
.task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; }
926 926
.task_done { background:#00c600 url(../images/task_done.png); border: 1px solid #00c600; }
927 927
.task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; }
928
.task_none { background:transparent; border-style: none; }
928 929

  
929 930
.task_todo.parent { background: #888; border: 1px solid #888; height: 3px;}
930 931
.task_late.parent, .task_done.parent { height: 3px;}
app/models/issue.rb
756 756
    dependencies
757 757
  end
758 758

  
759
  def all_precedes_issues
760
    dependencies = []
761
    relations_from.each do |relation|
762
      next unless relation.relation_type == IssueRelation::TYPE_PRECEDES
763
      dependencies << relation.issue_to
764
      dependencies += relation.issue_to.all_dependent_issues
765
    end
766
    dependencies
767
  end
768

  
759 769
  # Returns an array of issues that duplicate this one
760 770
  def duplicates
761 771
    relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
app/controllers/gantts_controller.rb
45 45
      format.pdf  { send_data(@gantt.to_pdf, :type => 'application/pdf', :filename => "#{basename}.pdf") }
46 46
    end
47 47
  end
48

  
49
  def edit_gantt
50
    date_from = Date.parse(params[:date_from])
51
    date_to = Date.parse(params[:date_to])
52
    months = date_to.month - date_from.month + 1
53
    params[:year] = date_from.year
54
    params[:month] = date_from.month
55
    params[:months] = months
56
    @gantt = Redmine::Helpers::Gantt.new(params)
57
    @gantt.project = @project
58
    text, status = @gantt.edit(params)
59
    render :text=>text, :status=>status
60
  end
61

  
62
  def find_optional_project
63
    begin
64
      if params[:action] && params[:action].to_s == "edit_gantt"
65
        @project = Project.find(params[:project_id]) unless params[:project_id].blank?
66
        allowed = User.current.allowed_to?(:edit_issues, @project, :global => true)
67
        if allowed
68
          return true
69
        else
70
          render :text => l(:text_edit_gantt_lack_of_permission), :status=>403
71
        end
72
      else
73
        super
74
      end
75
    rescue => e
76
      return e.to_s + "\n===\n" + [$!,$@.join("\n")].join("\n")
77
    end
78
  end
48 79
end
config/locales/en.yml
1008 1008
  text_account_destroy_confirmation: "Are you sure you want to proceed?\nYour account will be permanently deleted, with no way to reactivate it."
1009 1009
  text_session_expiration_settings: "Warning: changing these settings may expire the current sessions including yours."
1010 1010
  text_project_closed: This project is closed and read-only.
1011
  text_edit_gantt_lack_of_permission: lack of permission to edit issues
1011 1012

  
1012 1013
  default_role_manager: Manager
1013 1014
  default_role_developer: Developer
config/routes.rb
53 53

  
54 54
  match '/projects/:project_id/issues/gantt', :to => 'gantts#show'
55 55
  match '/issues/gantt', :to => 'gantts#show'
56
  match '/gantts/edit_gantt', :controller => 'gantts', :action => 'edit_gantt',
57
         :via => [:get]
58
  match '/gantts/edit_gantt/:id', :controller => 'gantts', :action => 'edit_gantt',
59
         :via => [:post]
56 60

  
57 61
  match '/projects/:project_id/issues/calendar', :to => 'calendars#show'
58 62
  match '/issues/calendar', :to => 'calendars#show'
test/integration/routing/gantts_test.rb
37 37
        { :controller => 'gantts', :action => 'show',
38 38
          :project_id => 'project-name', :format => 'pdf' }
39 39
      )
40
    assert_routing(
41
        { :method => 'get', :path => "/gantts/edit_gantt" },
42
        { :controller => 'gantts', :action => 'edit_gantt' }
43
      )
44
    assert_routing(
45
        { :method => 'post', :path => "/gantts/edit_gantt/123" },
46
        { :controller => 'gantts', :action => 'edit_gantt',
47
          :id => '123' }
48
      )
40 49
  end
41 50
end