##// END OF EJS Templates
Show precedes/follows and blocks/blocked relations on the Gantt diagram (#3436)....
Jean-Philippe Lang -
r10888:601148c5b11d
parent child
Show More
@@ -0,0 +1,114
1 var draw_gantt = null;
2 var draw_top;
3 var draw_right;
4 var draw_left;
5
6 var rels_stroke_width = 2;
7
8 function setDrawArea() {
9 draw_top = $("#gantt_draw_area").position().top;
10 draw_right = $("#gantt_draw_area").width();
11 draw_left = $("#gantt_area").scrollLeft();
12 }
13
14 function getRelationsArray() {
15 var arr = new Array();
16 $.each($('div.task_todo'), function(index_div, element) {
17 var element_id = $(element).attr("id");
18 if (element_id != null) {
19 var issue_id = element_id.replace("task-todo-issue-", "");
20 var data_rels = $(element).data("rels");
21 if (data_rels != null) {
22 for (rel_type_key in issue_relation_type) {
23 if (rel_type_key in data_rels) {
24 var issue_arr = data_rels[rel_type_key].toString().split(",");
25 $.each(issue_arr, function(index_issue, element_issue) {
26 arr.push({issue_from: issue_id, issue_to: element_issue,
27 rel_type: rel_type_key});
28 });
29 }
30 }
31 }
32 }
33 });
34 return arr;
35 }
36
37 function drawRelations() {
38 var arr = getRelationsArray();
39 $.each(arr, function(index_issue, element_issue) {
40 var issue_from = $("#task-todo-issue-" + element_issue["issue_from"]);
41 var issue_to = $("#task-todo-issue-" + element_issue["issue_to"]);
42 if (issue_from.size() == 0 || issue_to.size() == 0) {
43 return;
44 }
45 var issue_height = issue_from.height();
46 var issue_from_top = issue_from.position().top + (issue_height / 2) - draw_top;
47 var issue_from_right = issue_from.position().left + issue_from.width();
48 var issue_to_top = issue_to.position().top + (issue_height / 2) - draw_top;
49 var issue_to_left = issue_to.position().left;
50 var color = issue_relation_type[element_issue["rel_type"]]["color"];
51 var landscape_margin = issue_relation_type[element_issue["rel_type"]]["landscape_margin"];
52 var issue_from_right_rel = issue_from_right + landscape_margin;
53 var issue_to_left_rel = issue_to_left - landscape_margin;
54 draw_gantt.path(["M", issue_from_right + draw_left, issue_from_top,
55 "L", issue_from_right_rel + draw_left, issue_from_top])
56 .attr({stroke: color,
57 "stroke-width": rels_stroke_width
58 });
59 if (issue_from_right_rel < issue_to_left_rel) {
60 draw_gantt.path(["M", issue_from_right_rel + draw_left, issue_from_top,
61 "L", issue_from_right_rel + draw_left, issue_to_top])
62 .attr({stroke: color,
63 "stroke-width": rels_stroke_width
64 });
65 draw_gantt.path(["M", issue_from_right_rel + draw_left, issue_to_top,
66 "L", issue_to_left + draw_left, issue_to_top])
67 .attr({stroke: color,
68 "stroke-width": rels_stroke_width
69 });
70 } else {
71 var issue_middle_top = issue_to_top +
72 (issue_height *
73 ((issue_from_top > issue_to_top) ? 1 : -1));
74 draw_gantt.path(["M", issue_from_right_rel + draw_left, issue_from_top,
75 "L", issue_from_right_rel + draw_left, issue_middle_top])
76 .attr({stroke: color,
77 "stroke-width": rels_stroke_width
78 });
79 draw_gantt.path(["M", issue_from_right_rel + draw_left, issue_middle_top,
80 "L", issue_to_left_rel + draw_left, issue_middle_top])
81 .attr({stroke: color,
82 "stroke-width": rels_stroke_width
83 });
84 draw_gantt.path(["M", issue_to_left_rel + draw_left, issue_middle_top,
85 "L", issue_to_left_rel + draw_left, issue_to_top])
86 .attr({stroke: color,
87 "stroke-width": rels_stroke_width
88 });
89 draw_gantt.path(["M", issue_to_left_rel + draw_left, issue_to_top,
90 "L", issue_to_left + draw_left, issue_to_top])
91 .attr({stroke: color,
92 "stroke-width": rels_stroke_width
93 });
94 }
95 draw_gantt.path(["M", issue_to_left + draw_left, issue_to_top,
96 "l", -4 * rels_stroke_width, -2 * rels_stroke_width,
97 "l", 0, 4 * rels_stroke_width, "z"])
98 .attr({stroke: "none",
99 fill: color,
100 "stroke-linecap": "butt",
101 "stroke-linejoin": "miter",
102 });
103 });
104 }
105
106 function drawGanttHandler() {
107 var folder = document.getElementById('gantt_draw_area');
108 if(draw_gantt != null)
109 draw_gantt.clear();
110 else
111 draw_gantt = Raphael(folder);
112 setDrawArea();
113 drawRelations();
114 }
@@ -1,263 +1,282
1 1 <% @gantt.view = self %>
2 2 <h2><%= @query.new_record? ? l(:label_gantt) : h(@query.name) %></h2>
3 3
4 4 <%= form_tag({:controller => 'gantts', :action => 'show',
5 5 :project_id => @project, :month => params[:month],
6 6 :year => params[:year], :months => params[:months]},
7 7 :method => :get, :id => 'query_form') do %>
8 8 <%= hidden_field_tag 'set_filter', '1' %>
9 9 <fieldset id="filters" class="collapsible <%= @query.new_record? ? "" : "collapsed" %>">
10 10 <legend onclick="toggleFieldset(this);"><%= l(:label_filter_plural) %></legend>
11 11 <div style="<%= @query.new_record? ? "" : "display: none;" %>">
12 12 <%= render :partial => 'queries/filters', :locals => {:query => @query} %>
13 13 </div>
14 14 </fieldset>
15 15
16 16 <p class="contextual">
17 17 <%= gantt_zoom_link(@gantt, :in) %>
18 18 <%= gantt_zoom_link(@gantt, :out) %>
19 19 </p>
20 20
21 21 <p class="buttons">
22 22 <%= text_field_tag 'months', @gantt.months, :size => 2 %>
23 23 <%= l(:label_months_from) %>
24 24 <%= select_month(@gantt.month_from, :prefix => "month", :discard_type => true) %>
25 25 <%= select_year(@gantt.year_from, :prefix => "year", :discard_type => true) %>
26 26 <%= hidden_field_tag 'zoom', @gantt.zoom %>
27 27
28 28 <%= link_to_function l(:button_apply), '$("#query_form").submit()',
29 29 :class => 'icon icon-checked' %>
30 30 <%= link_to l(:button_clear), { :project_id => @project, :set_filter => 1 },
31 31 :class => 'icon icon-reload' %>
32 32 </p>
33 33 <% end %>
34 34
35 35 <%= error_messages_for 'query' %>
36 36 <% if @query.valid? %>
37 37 <%
38 38 zoom = 1
39 39 @gantt.zoom.times { zoom = zoom * 2 }
40 40
41 41 subject_width = 330
42 42 header_heigth = 18
43 43
44 44 headers_height = header_heigth
45 45 show_weeks = false
46 46 show_days = false
47 47
48 48 if @gantt.zoom > 1
49 49 show_weeks = true
50 50 headers_height = 2 * header_heigth
51 51 if @gantt.zoom > 2
52 52 show_days = true
53 53 headers_height = 3 * header_heigth
54 54 end
55 55 end
56 56
57 57 # Width of the entire chart
58 58 g_width = ((@gantt.date_to - @gantt.date_from + 1) * zoom).to_i
59 59 @gantt.render(:top => headers_height + 8,
60 60 :zoom => zoom,
61 61 :g_width => g_width,
62 62 :subject_width => subject_width)
63 63 g_height = [(20 * (@gantt.number_of_rows + 6)) + 150, 206].max
64 64 t_height = g_height + headers_height
65 65 %>
66 66
67 67 <% if @gantt.truncated %>
68 68 <p class="warning"><%= l(:notice_gantt_chart_truncated, :max => @gantt.max_rows) %></p>
69 69 <% end %>
70 70
71 71 <table style="width:100%; border:0; border-collapse: collapse;">
72 72 <tr>
73 73 <td style="width:<%= subject_width %>px; padding:0px;">
74 74 <%
75 75 style = ""
76 76 style += "position:relative;"
77 77 style += "height: #{t_height + 24}px;"
78 78 style += "width: #{subject_width + 1}px;"
79 79 %>
80 80 <%= content_tag(:div, :style => style) do %>
81 81 <%
82 82 style = ""
83 83 style += "right:-2px;"
84 84 style += "width: #{subject_width}px;"
85 85 style += "height: #{headers_height}px;"
86 86 style += 'background: #eee;'
87 87 %>
88 88 <%= content_tag(:div, "", :style => style, :class => "gantt_hdr") %>
89 89 <%
90 90 style = ""
91 91 style += "right:-2px;"
92 92 style += "width: #{subject_width}px;"
93 93 style += "height: #{t_height}px;"
94 94 style += 'border-left: 1px solid #c0c0c0;'
95 95 style += 'overflow: hidden;'
96 96 %>
97 97 <%= content_tag(:div, "", :style => style, :class => "gantt_hdr") %>
98 98 <%= content_tag(:div, :class => "gantt_subjects") do %>
99 99 <%= @gantt.subjects.html_safe %>
100 100 <% end %>
101 101 <% end %>
102 102 </td>
103 103
104 104 <td>
105 <div style="position:relative;height:<%= t_height + 24 %>px;overflow:auto;">
105 <div style="position:relative;height:<%= t_height + 24 %>px;overflow:auto;" id="gantt_area">
106 106 <%
107 107 style = ""
108 108 style += "width: #{g_width - 1}px;"
109 109 style += "height: #{headers_height}px;"
110 110 style += 'background: #eee;'
111 111 %>
112 112 <%= content_tag(:div, '&nbsp;'.html_safe, :style => style, :class => "gantt_hdr") %>
113 113
114 114 <% ###### Months headers ###### %>
115 115 <%
116 116 month_f = @gantt.date_from
117 117 left = 0
118 118 height = (show_weeks ? header_heigth : header_heigth + g_height)
119 119 %>
120 120 <% @gantt.months.times do %>
121 121 <%
122 122 width = (((month_f >> 1) - month_f) * zoom - 1).to_i
123 123 style = ""
124 124 style += "left: #{left}px;"
125 125 style += "width: #{width}px;"
126 126 style += "height: #{height}px;"
127 127 %>
128 128 <%= content_tag(:div, :style => style, :class => "gantt_hdr") do %>
129 129 <%= link_to h("#{month_f.year}-#{month_f.month}"),
130 130 @gantt.params.merge(:year => month_f.year, :month => month_f.month),
131 131 :title => "#{month_name(month_f.month)} #{month_f.year}" %>
132 132 <% end %>
133 133 <%
134 134 left = left + width + 1
135 135 month_f = month_f >> 1
136 136 %>
137 137 <% end %>
138 138
139 139 <% ###### Weeks headers ###### %>
140 140 <% if show_weeks %>
141 141 <%
142 142 left = 0
143 143 height = (show_days ? header_heigth - 1 : header_heigth - 1 + g_height)
144 144 %>
145 145 <% if @gantt.date_from.cwday == 1 %>
146 146 <%
147 147 # @date_from is monday
148 148 week_f = @gantt.date_from
149 149 %>
150 150 <% else %>
151 151 <%
152 152 # find next monday after @date_from
153 153 week_f = @gantt.date_from + (7 - @gantt.date_from.cwday + 1)
154 154 width = (7 - @gantt.date_from.cwday + 1) * zoom - 1
155 155 style = ""
156 156 style += "left: #{left}px;"
157 157 style += "top: 19px;"
158 158 style += "width: #{width}px;"
159 159 style += "height: #{height}px;"
160 160 %>
161 161 <%= content_tag(:div, '&nbsp;'.html_safe,
162 162 :style => style, :class => "gantt_hdr") %>
163 163 <% left = left + width + 1 %>
164 164 <% end %>
165 165 <% while week_f <= @gantt.date_to %>
166 166 <%
167 167 width = ((week_f + 6 <= @gantt.date_to) ?
168 168 7 * zoom - 1 :
169 169 (@gantt.date_to - week_f + 1) * zoom - 1).to_i
170 170 style = ""
171 171 style += "left: #{left}px;"
172 172 style += "top: 19px;"
173 173 style += "width: #{width}px;"
174 174 style += "height: #{height}px;"
175 175 %>
176 176 <%= content_tag(:div, :style => style, :class => "gantt_hdr") do %>
177 177 <%= content_tag(:small) do %>
178 178 <%= week_f.cweek if width >= 16 %>
179 179 <% end %>
180 180 <% end %>
181 181 <%
182 182 left = left + width + 1
183 183 week_f = week_f + 7
184 184 %>
185 185 <% end %>
186 186 <% end %>
187 187
188 188 <% ###### Days headers ####### %>
189 189 <% if show_days %>
190 190 <%
191 191 left = 0
192 192 height = g_height + header_heigth - 1
193 193 wday = @gantt.date_from.cwday
194 194 %>
195 195 <% (@gantt.date_to - @gantt.date_from + 1).to_i.times do %>
196 196 <%
197 197 width = zoom - 1
198 198 style = ""
199 199 style += "left: #{left}px;"
200 200 style += "top:37px;"
201 201 style += "width: #{width}px;"
202 202 style += "height: #{height}px;"
203 203 style += "font-size:0.7em;"
204 204 clss = "gantt_hdr"
205 205 clss << " nwday" if @gantt.non_working_week_days.include?(wday)
206 206 %>
207 207 <%= content_tag(:div, :style => style, :class => clss) do %>
208 208 <%= day_letter(wday) %>
209 209 <% end %>
210 210 <%
211 211 left = left + width + 1
212 212 wday = wday + 1
213 213 wday = 1 if wday > 7
214 214 %>
215 215 <% end %>
216 216 <% end %>
217 217
218 218 <%= @gantt.lines.html_safe %>
219 219
220 220 <% ###### Today red line (excluded from cache) ###### %>
221 221 <% if Date.today >= @gantt.date_from and Date.today <= @gantt.date_to %>
222 222 <%
223 223 today_left = (((Date.today - @gantt.date_from + 1) * zoom).floor() - 1).to_i
224 224 style = ""
225 225 style += "position: absolute;"
226 226 style += "height: #{g_height}px;"
227 227 style += "top: #{headers_height + 1}px;"
228 228 style += "left: #{today_left}px;"
229 229 style += "width:10px;"
230 230 style += "border-left: 1px dashed red;"
231 231 %>
232 232 <%= content_tag(:div, '&nbsp;'.html_safe, :style => style) %>
233 233 <% end %>
234
234 <%
235 style = ""
236 style += "position: absolute;"
237 style += "height: #{g_height}px;"
238 style += "top: #{headers_height + 1}px;"
239 style += "left: 0px;"
240 style += "width: #{g_width - 1}px;"
241 %>
242 <%= content_tag(:div, '', :style => style, :id => "gantt_draw_area") %>
235 243 </div>
236 244 </td>
237 245 </tr>
238 246 </table>
239 247
240 248 <table style="width:100%">
241 249 <tr>
242 250 <td align="left">
243 251 <%= link_to_content_update("\xc2\xab " + l(:label_previous),
244 252 params.merge(@gantt.params_previous)) %>
245 253 </td>
246 254 <td align="right">
247 255 <%= link_to_content_update(l(:label_next) + " \xc2\xbb",
248 256 params.merge(@gantt.params_next)) %>
249 257 </td>
250 258 </tr>
251 259 </table>
252 260
253 261 <% other_formats_links do |f| %>
254 262 <%= f.link_to 'PDF', :url => params.merge(@gantt.params) %>
255 263 <%= f.link_to('PNG', :url => params.merge(@gantt.params)) if @gantt.respond_to?('to_image') %>
256 264 <% end %>
257 265 <% end # query.valid? %>
258 266
259 267 <% content_for :sidebar do %>
260 268 <%= render :partial => 'issues/sidebar' %>
261 269 <% end %>
262 270
263 271 <% html_title(l(:label_gantt)) -%>
272
273 <% content_for :header_tags do %>
274 <%= javascript_include_tag 'raphael' %>
275 <%= javascript_include_tag 'gantt' %>
276 <% end %>
277
278 <%= javascript_tag do %>
279 var issue_relation_type = <%= raw Redmine::Helpers::Gantt::DRAW_TYPES.to_json %>;
280 $(document).ready(drawGanttHandler);
281 $(window).resize(drawGanttHandler);
282 <% end %>
@@ -1,883 +1,922
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 module Redmine
19 19 module Helpers
20 20 # Simple class to handle gantt chart data
21 21 class Gantt
22 22 include ERB::Util
23 23 include Redmine::I18n
24 24 include Redmine::Utils::DateCalculation
25 25
26 # Relation types that are rendered
27 DRAW_TYPES = {
28 IssueRelation::TYPE_BLOCKS => { :landscape_margin => 16, :color => '#F34F4F' },
29 IssueRelation::TYPE_PRECEDES => { :landscape_margin => 20, :color => '#628FEA' }
30 }.freeze
31
26 32 # :nodoc:
27 33 # Some utility methods for the PDF export
28 34 class PDF
29 35 MaxCharactorsForSubject = 45
30 36 TotalWidth = 280
31 37 LeftPaneWidth = 100
32 38
33 39 def self.right_pane_width
34 40 TotalWidth - LeftPaneWidth
35 41 end
36 42 end
37 43
38 44 attr_reader :year_from, :month_from, :date_from, :date_to, :zoom, :months, :truncated, :max_rows
39 45 attr_accessor :query
40 46 attr_accessor :project
41 47 attr_accessor :view
42 48
43 49 def initialize(options={})
44 50 options = options.dup
45 51 if options[:year] && options[:year].to_i >0
46 52 @year_from = options[:year].to_i
47 53 if options[:month] && options[:month].to_i >=1 && options[:month].to_i <= 12
48 54 @month_from = options[:month].to_i
49 55 else
50 56 @month_from = 1
51 57 end
52 58 else
53 59 @month_from ||= Date.today.month
54 60 @year_from ||= Date.today.year
55 61 end
56 62 zoom = (options[:zoom] || User.current.pref[:gantt_zoom]).to_i
57 63 @zoom = (zoom > 0 && zoom < 5) ? zoom : 2
58 64 months = (options[:months] || User.current.pref[:gantt_months]).to_i
59 65 @months = (months > 0 && months < 25) ? months : 6
60 66 # Save gantt parameters as user preference (zoom and months count)
61 67 if (User.current.logged? && (@zoom != User.current.pref[:gantt_zoom] ||
62 68 @months != User.current.pref[:gantt_months]))
63 69 User.current.pref[:gantt_zoom], User.current.pref[:gantt_months] = @zoom, @months
64 70 User.current.preference.save
65 71 end
66 72 @date_from = Date.civil(@year_from, @month_from, 1)
67 73 @date_to = (@date_from >> @months) - 1
68 74 @subjects = ''
69 75 @lines = ''
70 76 @number_of_rows = nil
71 77 @issue_ancestors = []
72 78 @truncated = false
73 79 if options.has_key?(:max_rows)
74 80 @max_rows = options[:max_rows]
75 81 else
76 82 @max_rows = Setting.gantt_items_limit.blank? ? nil : Setting.gantt_items_limit.to_i
77 83 end
78 84 end
79 85
80 86 def common_params
81 87 { :controller => 'gantts', :action => 'show', :project_id => @project }
82 88 end
83 89
84 90 def params
85 91 common_params.merge({:zoom => zoom, :year => year_from,
86 92 :month => month_from, :months => months})
87 93 end
88 94
89 95 def params_previous
90 96 common_params.merge({:year => (date_from << months).year,
91 97 :month => (date_from << months).month,
92 98 :zoom => zoom, :months => months})
93 99 end
94 100
95 101 def params_next
96 102 common_params.merge({:year => (date_from >> months).year,
97 103 :month => (date_from >> months).month,
98 104 :zoom => zoom, :months => months})
99 105 end
100 106
101 107 # Returns the number of rows that will be rendered on the Gantt chart
102 108 def number_of_rows
103 109 return @number_of_rows if @number_of_rows
104 110 rows = projects.inject(0) {|total, p| total += number_of_rows_on_project(p)}
105 111 rows > @max_rows ? @max_rows : rows
106 112 end
107 113
108 114 # Returns the number of rows that will be used to list a project on
109 115 # the Gantt chart. This will recurse for each subproject.
110 116 def number_of_rows_on_project(project)
111 117 return 0 unless projects.include?(project)
112 118 count = 1
113 119 count += project_issues(project).size
114 120 count += project_versions(project).size
115 121 count
116 122 end
117 123
118 124 # Renders the subjects of the Gantt chart, the left side.
119 125 def subjects(options={})
120 126 render(options.merge(:only => :subjects)) unless @subjects_rendered
121 127 @subjects
122 128 end
123 129
124 130 # Renders the lines of the Gantt chart, the right side
125 131 def lines(options={})
126 132 render(options.merge(:only => :lines)) unless @lines_rendered
127 133 @lines
128 134 end
129 135
130 136 # Returns issues that will be rendered
131 137 def issues
132 138 @issues ||= @query.issues(
133 139 :include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
134 140 :order => "#{Project.table_name}.lft ASC, #{Issue.table_name}.id ASC",
135 141 :limit => @max_rows
136 142 )
137 143 end
138 144
145 # Returns a hash of the relations between the issues that are present on the gantt
146 # and that should be displayed, grouped by issue ids.
147 def relations
148 return @relations if @relations
149 if issues.any?
150 issue_ids = issues.map(&:id)
151 @relations = IssueRelation.
152 where(:issue_from_id => issue_ids, :issue_to_id => issue_ids, :relation_type => DRAW_TYPES.keys).
153 group_by(&:issue_from_id)
154 else
155 @relations = {}
156 end
157 end
158
139 159 # Return all the project nodes that will be displayed
140 160 def projects
141 161 return @projects if @projects
142 162 ids = issues.collect(&:project).uniq.collect(&:id)
143 163 if ids.any?
144 164 # All issues projects and their visible ancestors
145 165 @projects = Project.visible.all(
146 166 :joins => "LEFT JOIN #{Project.table_name} child ON #{Project.table_name}.lft <= child.lft AND #{Project.table_name}.rgt >= child.rgt",
147 167 :conditions => ["child.id IN (?)", ids],
148 168 :order => "#{Project.table_name}.lft ASC"
149 169 ).uniq
150 170 else
151 171 @projects = []
152 172 end
153 173 end
154 174
155 175 # Returns the issues that belong to +project+
156 176 def project_issues(project)
157 177 @issues_by_project ||= issues.group_by(&:project)
158 178 @issues_by_project[project] || []
159 179 end
160 180
161 181 # Returns the distinct versions of the issues that belong to +project+
162 182 def project_versions(project)
163 183 project_issues(project).collect(&:fixed_version).compact.uniq
164 184 end
165 185
166 186 # Returns the issues that belong to +project+ and are assigned to +version+
167 187 def version_issues(project, version)
168 188 project_issues(project).select {|issue| issue.fixed_version == version}
169 189 end
170 190
171 191 def render(options={})
172 192 options = {:top => 0, :top_increment => 20,
173 193 :indent_increment => 20, :render => :subject,
174 194 :format => :html}.merge(options)
175 195 indent = options[:indent] || 4
176 196 @subjects = '' unless options[:only] == :lines
177 197 @lines = '' unless options[:only] == :subjects
178 198 @number_of_rows = 0
179 199 Project.project_tree(projects) do |project, level|
180 200 options[:indent] = indent + level * options[:indent_increment]
181 201 render_project(project, options)
182 202 break if abort?
183 203 end
184 204 @subjects_rendered = true unless options[:only] == :lines
185 205 @lines_rendered = true unless options[:only] == :subjects
186 206 render_end(options)
187 207 end
188 208
189 209 def render_project(project, options={})
190 210 subject_for_project(project, options) unless options[:only] == :lines
191 211 line_for_project(project, options) unless options[:only] == :subjects
192 212 options[:top] += options[:top_increment]
193 213 options[:indent] += options[:indent_increment]
194 214 @number_of_rows += 1
195 215 return if abort?
196 216 issues = project_issues(project).select {|i| i.fixed_version.nil?}
197 217 sort_issues!(issues)
198 218 if issues
199 219 render_issues(issues, options)
200 220 return if abort?
201 221 end
202 222 versions = project_versions(project)
203 223 versions.each do |version|
204 224 render_version(project, version, options)
205 225 end
206 226 # Remove indent to hit the next sibling
207 227 options[:indent] -= options[:indent_increment]
208 228 end
209 229
210 230 def render_issues(issues, options={})
211 231 @issue_ancestors = []
212 232 issues.each do |i|
213 233 subject_for_issue(i, options) unless options[:only] == :lines
214 234 line_for_issue(i, options) unless options[:only] == :subjects
215 235 options[:top] += options[:top_increment]
216 236 @number_of_rows += 1
217 237 break if abort?
218 238 end
219 239 options[:indent] -= (options[:indent_increment] * @issue_ancestors.size)
220 240 end
221 241
222 242 def render_version(project, version, options={})
223 243 # Version header
224 244 subject_for_version(version, options) unless options[:only] == :lines
225 245 line_for_version(version, options) unless options[:only] == :subjects
226 246 options[:top] += options[:top_increment]
227 247 @number_of_rows += 1
228 248 return if abort?
229 249 issues = version_issues(project, version)
230 250 if issues
231 251 sort_issues!(issues)
232 252 # Indent issues
233 253 options[:indent] += options[:indent_increment]
234 254 render_issues(issues, options)
235 255 options[:indent] -= options[:indent_increment]
236 256 end
237 257 end
238 258
239 259 def render_end(options={})
240 260 case options[:format]
241 261 when :pdf
242 262 options[:pdf].Line(15, options[:top], PDF::TotalWidth, options[:top])
243 263 end
244 264 end
245 265
246 266 def subject_for_project(project, options)
247 267 case options[:format]
248 268 when :html
249 269 html_class = ""
250 270 html_class << 'icon icon-projects '
251 271 html_class << (project.overdue? ? 'project-overdue' : '')
252 272 s = view.link_to_project(project).html_safe
253 273 subject = view.content_tag(:span, s,
254 274 :class => html_class).html_safe
255 275 html_subject(options, subject, :css => "project-name")
256 276 when :image
257 277 image_subject(options, project.name)
258 278 when :pdf
259 279 pdf_new_page?(options)
260 280 pdf_subject(options, project.name)
261 281 end
262 282 end
263 283
264 284 def line_for_project(project, options)
265 285 # Skip versions that don't have a start_date or due date
266 286 if project.is_a?(Project) && project.start_date && project.due_date
267 287 options[:zoom] ||= 1
268 288 options[:g_width] ||= (self.date_to - self.date_from + 1) * options[:zoom]
269 289 coords = coordinates(project.start_date, project.due_date, nil, options[:zoom])
270 290 label = h(project)
271 291 case options[:format]
272 292 when :html
273 293 html_task(options, coords, :css => "project task", :label => label, :markers => true)
274 294 when :image
275 295 image_task(options, coords, :label => label, :markers => true, :height => 3)
276 296 when :pdf
277 297 pdf_task(options, coords, :label => label, :markers => true, :height => 0.8)
278 298 end
279 299 else
280 300 ActiveRecord::Base.logger.debug "Gantt#line_for_project was not given a project with a start_date"
281 301 ''
282 302 end
283 303 end
284 304
285 305 def subject_for_version(version, options)
286 306 case options[:format]
287 307 when :html
288 308 html_class = ""
289 309 html_class << 'icon icon-package '
290 310 html_class << (version.behind_schedule? ? 'version-behind-schedule' : '') << " "
291 311 html_class << (version.overdue? ? 'version-overdue' : '')
292 312 s = view.link_to_version(version).html_safe
293 313 subject = view.content_tag(:span, s,
294 314 :class => html_class).html_safe
295 315 html_subject(options, subject, :css => "version-name")
296 316 when :image
297 317 image_subject(options, version.to_s_with_project)
298 318 when :pdf
299 319 pdf_new_page?(options)
300 320 pdf_subject(options, version.to_s_with_project)
301 321 end
302 322 end
303 323
304 324 def line_for_version(version, options)
305 325 # Skip versions that don't have a start_date
306 326 if version.is_a?(Version) && version.start_date && version.due_date
307 327 options[:zoom] ||= 1
308 328 options[:g_width] ||= (self.date_to - self.date_from + 1) * options[:zoom]
309 329 coords = coordinates(version.start_date,
310 330 version.due_date, version.completed_percent,
311 331 options[:zoom])
312 332 label = "#{h version} #{h version.completed_percent.to_i.to_s}%"
313 333 label = h("#{version.project} -") + label unless @project && @project == version.project
314 334 case options[:format]
315 335 when :html
316 336 html_task(options, coords, :css => "version task", :label => label, :markers => true)
317 337 when :image
318 338 image_task(options, coords, :label => label, :markers => true, :height => 3)
319 339 when :pdf
320 340 pdf_task(options, coords, :label => label, :markers => true, :height => 0.8)
321 341 end
322 342 else
323 343 ActiveRecord::Base.logger.debug "Gantt#line_for_version was not given a version with a start_date"
324 344 ''
325 345 end
326 346 end
327 347
328 348 def subject_for_issue(issue, options)
329 349 while @issue_ancestors.any? && !issue.is_descendant_of?(@issue_ancestors.last)
330 350 @issue_ancestors.pop
331 351 options[:indent] -= options[:indent_increment]
332 352 end
333 353 output = case options[:format]
334 354 when :html
335 355 css_classes = ''
336 356 css_classes << ' issue-overdue' if issue.overdue?
337 357 css_classes << ' issue-behind-schedule' if issue.behind_schedule?
338 358 css_classes << ' icon icon-issue' unless Setting.gravatar_enabled? && issue.assigned_to
339 359 s = "".html_safe
340 360 if issue.assigned_to.present?
341 361 assigned_string = l(:field_assigned_to) + ": " + issue.assigned_to.name
342 362 s << view.avatar(issue.assigned_to,
343 363 :class => 'gravatar icon-gravatar',
344 364 :size => 10,
345 365 :title => assigned_string).to_s.html_safe
346 366 end
347 367 s << view.link_to_issue(issue).html_safe
348 368 subject = view.content_tag(:span, s, :class => css_classes).html_safe
349 369 html_subject(options, subject, :css => "issue-subject",
350 370 :title => issue.subject) + "\n"
351 371 when :image
352 372 image_subject(options, issue.subject)
353 373 when :pdf
354 374 pdf_new_page?(options)
355 375 pdf_subject(options, issue.subject)
356 376 end
357 377 unless issue.leaf?
358 378 @issue_ancestors << issue
359 379 options[:indent] += options[:indent_increment]
360 380 end
361 381 output
362 382 end
363 383
364 384 def line_for_issue(issue, options)
365 385 # Skip issues that don't have a due_before (due_date or version's due_date)
366 386 if issue.is_a?(Issue) && issue.due_before
367 387 coords = coordinates(issue.start_date, issue.due_before, issue.done_ratio, options[:zoom])
368 388 label = "#{issue.status.name} #{issue.done_ratio}%"
369 389 case options[:format]
370 390 when :html
371 391 html_task(options, coords,
372 392 :css => "task " + (issue.leaf? ? 'leaf' : 'parent'),
373 393 :label => label, :issue => issue,
374 394 :markers => !issue.leaf?)
375 395 when :image
376 396 image_task(options, coords, :label => label)
377 397 when :pdf
378 398 pdf_task(options, coords, :label => label)
379 399 end
380 400 else
381 401 ActiveRecord::Base.logger.debug "GanttHelper#line_for_issue was not given an issue with a due_before"
382 402 ''
383 403 end
384 404 end
385 405
386 406 # Generates a gantt image
387 407 # Only defined if RMagick is avalaible
388 408 def to_image(format='PNG')
389 409 date_to = (@date_from >> @months) - 1
390 410 show_weeks = @zoom > 1
391 411 show_days = @zoom > 2
392 412 subject_width = 400
393 413 header_height = 18
394 414 # width of one day in pixels
395 415 zoom = @zoom * 2
396 416 g_width = (@date_to - @date_from + 1) * zoom
397 417 g_height = 20 * number_of_rows + 30
398 418 headers_height = (show_weeks ? 2 * header_height : header_height)
399 419 height = g_height + headers_height
400 420 imgl = Magick::ImageList.new
401 421 imgl.new_image(subject_width + g_width + 1, height)
402 422 gc = Magick::Draw.new
403 423 gc.font = Redmine::Configuration['rmagick_font_path'] || ""
404 424 # Subjects
405 425 gc.stroke('transparent')
406 426 subjects(:image => gc, :top => (headers_height + 20), :indent => 4, :format => :image)
407 427 # Months headers
408 428 month_f = @date_from
409 429 left = subject_width
410 430 @months.times do
411 431 width = ((month_f >> 1) - month_f) * zoom
412 432 gc.fill('white')
413 433 gc.stroke('grey')
414 434 gc.stroke_width(1)
415 435 gc.rectangle(left, 0, left + width, height)
416 436 gc.fill('black')
417 437 gc.stroke('transparent')
418 438 gc.stroke_width(1)
419 439 gc.text(left.round + 8, 14, "#{month_f.year}-#{month_f.month}")
420 440 left = left + width
421 441 month_f = month_f >> 1
422 442 end
423 443 # Weeks headers
424 444 if show_weeks
425 445 left = subject_width
426 446 height = header_height
427 447 if @date_from.cwday == 1
428 448 # date_from is monday
429 449 week_f = date_from
430 450 else
431 451 # find next monday after date_from
432 452 week_f = @date_from + (7 - @date_from.cwday + 1)
433 453 width = (7 - @date_from.cwday + 1) * zoom
434 454 gc.fill('white')
435 455 gc.stroke('grey')
436 456 gc.stroke_width(1)
437 457 gc.rectangle(left, header_height, left + width, 2 * header_height + g_height - 1)
438 458 left = left + width
439 459 end
440 460 while week_f <= date_to
441 461 width = (week_f + 6 <= date_to) ? 7 * zoom : (date_to - week_f + 1) * zoom
442 462 gc.fill('white')
443 463 gc.stroke('grey')
444 464 gc.stroke_width(1)
445 465 gc.rectangle(left.round, header_height, left.round + width, 2 * header_height + g_height - 1)
446 466 gc.fill('black')
447 467 gc.stroke('transparent')
448 468 gc.stroke_width(1)
449 469 gc.text(left.round + 2, header_height + 14, week_f.cweek.to_s)
450 470 left = left + width
451 471 week_f = week_f + 7
452 472 end
453 473 end
454 474 # Days details (week-end in grey)
455 475 if show_days
456 476 left = subject_width
457 477 height = g_height + header_height - 1
458 478 wday = @date_from.cwday
459 479 (date_to - @date_from + 1).to_i.times do
460 480 width = zoom
461 481 gc.fill(non_working_week_days.include?(wday) ? '#eee' : 'white')
462 482 gc.stroke('#ddd')
463 483 gc.stroke_width(1)
464 484 gc.rectangle(left, 2 * header_height, left + width, 2 * header_height + g_height - 1)
465 485 left = left + width
466 486 wday = wday + 1
467 487 wday = 1 if wday > 7
468 488 end
469 489 end
470 490 # border
471 491 gc.fill('transparent')
472 492 gc.stroke('grey')
473 493 gc.stroke_width(1)
474 494 gc.rectangle(0, 0, subject_width + g_width, headers_height)
475 495 gc.stroke('black')
476 496 gc.rectangle(0, 0, subject_width + g_width, g_height + headers_height - 1)
477 497 # content
478 498 top = headers_height + 20
479 499 gc.stroke('transparent')
480 500 lines(:image => gc, :top => top, :zoom => zoom,
481 501 :subject_width => subject_width, :format => :image)
482 502 # today red line
483 503 if Date.today >= @date_from and Date.today <= date_to
484 504 gc.stroke('red')
485 505 x = (Date.today - @date_from + 1) * zoom + subject_width
486 506 gc.line(x, headers_height, x, headers_height + g_height - 1)
487 507 end
488 508 gc.draw(imgl)
489 509 imgl.format = format
490 510 imgl.to_blob
491 511 end if Object.const_defined?(:Magick)
492 512
493 513 def to_pdf
494 514 pdf = ::Redmine::Export::PDF::ITCPDF.new(current_language)
495 515 pdf.SetTitle("#{l(:label_gantt)} #{project}")
496 516 pdf.alias_nb_pages
497 517 pdf.footer_date = format_date(Date.today)
498 518 pdf.AddPage("L")
499 519 pdf.SetFontStyle('B', 12)
500 520 pdf.SetX(15)
501 521 pdf.RDMCell(PDF::LeftPaneWidth, 20, project.to_s)
502 522 pdf.Ln
503 523 pdf.SetFontStyle('B', 9)
504 524 subject_width = PDF::LeftPaneWidth
505 525 header_height = 5
506 526 headers_height = header_height
507 527 show_weeks = false
508 528 show_days = false
509 529 if self.months < 7
510 530 show_weeks = true
511 531 headers_height = 2 * header_height
512 532 if self.months < 3
513 533 show_days = true
514 534 headers_height = 3 * header_height
515 535 end
516 536 end
517 537 g_width = PDF.right_pane_width
518 538 zoom = (g_width) / (self.date_to - self.date_from + 1)
519 539 g_height = 120
520 540 t_height = g_height + headers_height
521 541 y_start = pdf.GetY
522 542 # Months headers
523 543 month_f = self.date_from
524 544 left = subject_width
525 545 height = header_height
526 546 self.months.times do
527 547 width = ((month_f >> 1) - month_f) * zoom
528 548 pdf.SetY(y_start)
529 549 pdf.SetX(left)
530 550 pdf.RDMCell(width, height, "#{month_f.year}-#{month_f.month}", "LTR", 0, "C")
531 551 left = left + width
532 552 month_f = month_f >> 1
533 553 end
534 554 # Weeks headers
535 555 if show_weeks
536 556 left = subject_width
537 557 height = header_height
538 558 if self.date_from.cwday == 1
539 559 # self.date_from is monday
540 560 week_f = self.date_from
541 561 else
542 562 # find next monday after self.date_from
543 563 week_f = self.date_from + (7 - self.date_from.cwday + 1)
544 564 width = (7 - self.date_from.cwday + 1) * zoom-1
545 565 pdf.SetY(y_start + header_height)
546 566 pdf.SetX(left)
547 567 pdf.RDMCell(width + 1, height, "", "LTR")
548 568 left = left + width + 1
549 569 end
550 570 while week_f <= self.date_to
551 571 width = (week_f + 6 <= self.date_to) ? 7 * zoom : (self.date_to - week_f + 1) * zoom
552 572 pdf.SetY(y_start + header_height)
553 573 pdf.SetX(left)
554 574 pdf.RDMCell(width, height, (width >= 5 ? week_f.cweek.to_s : ""), "LTR", 0, "C")
555 575 left = left + width
556 576 week_f = week_f + 7
557 577 end
558 578 end
559 579 # Days headers
560 580 if show_days
561 581 left = subject_width
562 582 height = header_height
563 583 wday = self.date_from.cwday
564 584 pdf.SetFontStyle('B', 7)
565 585 (self.date_to - self.date_from + 1).to_i.times do
566 586 width = zoom
567 587 pdf.SetY(y_start + 2 * header_height)
568 588 pdf.SetX(left)
569 589 pdf.RDMCell(width, height, day_name(wday).first, "LTR", 0, "C")
570 590 left = left + width
571 591 wday = wday + 1
572 592 wday = 1 if wday > 7
573 593 end
574 594 end
575 595 pdf.SetY(y_start)
576 596 pdf.SetX(15)
577 597 pdf.RDMCell(subject_width + g_width - 15, headers_height, "", 1)
578 598 # Tasks
579 599 top = headers_height + y_start
580 600 options = {
581 601 :top => top,
582 602 :zoom => zoom,
583 603 :subject_width => subject_width,
584 604 :g_width => g_width,
585 605 :indent => 0,
586 606 :indent_increment => 5,
587 607 :top_increment => 5,
588 608 :format => :pdf,
589 609 :pdf => pdf
590 610 }
591 611 render(options)
592 612 pdf.Output
593 613 end
594 614
595 615 private
596 616
597 617 def coordinates(start_date, end_date, progress, zoom=nil)
598 618 zoom ||= @zoom
599 619 coords = {}
600 620 if start_date && end_date && start_date < self.date_to && end_date > self.date_from
601 621 if start_date > self.date_from
602 622 coords[:start] = start_date - self.date_from
603 623 coords[:bar_start] = start_date - self.date_from
604 624 else
605 625 coords[:bar_start] = 0
606 626 end
607 627 if end_date < self.date_to
608 628 coords[:end] = end_date - self.date_from
609 629 coords[:bar_end] = end_date - self.date_from + 1
610 630 else
611 631 coords[:bar_end] = self.date_to - self.date_from + 1
612 632 end
613 633 if progress
614 634 progress_date = start_date + (end_date - start_date + 1) * (progress / 100.0)
615 635 if progress_date > self.date_from && progress_date > start_date
616 636 if progress_date < self.date_to
617 637 coords[:bar_progress_end] = progress_date - self.date_from
618 638 else
619 639 coords[:bar_progress_end] = self.date_to - self.date_from + 1
620 640 end
621 641 end
622 642 if progress_date < Date.today
623 643 late_date = [Date.today, end_date].min
624 644 if late_date > self.date_from && late_date > start_date
625 645 if late_date < self.date_to
626 646 coords[:bar_late_end] = late_date - self.date_from + 1
627 647 else
628 648 coords[:bar_late_end] = self.date_to - self.date_from + 1
629 649 end
630 650 end
631 651 end
632 652 end
633 653 end
634 654 # Transforms dates into pixels witdh
635 655 coords.keys.each do |key|
636 656 coords[key] = (coords[key] * zoom).floor
637 657 end
638 658 coords
639 659 end
640 660
641 661 # Sorts a collection of issues by start_date, due_date, id for gantt rendering
642 662 def sort_issues!(issues)
643 663 issues.sort! { |a, b| gantt_issue_compare(a, b) }
644 664 end
645 665
646 666 # TODO: top level issues should be sorted by start date
647 667 def gantt_issue_compare(x, y)
648 668 if x.root_id == y.root_id
649 669 x.lft <=> y.lft
650 670 else
651 671 x.root_id <=> y.root_id
652 672 end
653 673 end
654 674
655 675 def current_limit
656 676 if @max_rows
657 677 @max_rows - @number_of_rows
658 678 else
659 679 nil
660 680 end
661 681 end
662 682
663 683 def abort?
664 684 if @max_rows && @number_of_rows >= @max_rows
665 685 @truncated = true
666 686 end
667 687 end
668 688
669 689 def pdf_new_page?(options)
670 690 if options[:top] > 180
671 691 options[:pdf].Line(15, options[:top], PDF::TotalWidth, options[:top])
672 692 options[:pdf].AddPage("L")
673 693 options[:top] = 15
674 694 options[:pdf].Line(15, options[:top] - 0.1, PDF::TotalWidth, options[:top] - 0.1)
675 695 end
676 696 end
677 697
678 698 def html_subject(params, subject, options={})
679 699 style = "position: absolute;top:#{params[:top]}px;left:#{params[:indent]}px;"
680 700 style << "width:#{params[:subject_width] - params[:indent]}px;" if params[:subject_width]
681 701 output = view.content_tag('div', subject,
682 702 :class => options[:css], :style => style,
683 703 :title => options[:title])
684 704 @subjects << output
685 705 output
686 706 end
687 707
688 708 def pdf_subject(params, subject, options={})
689 709 params[:pdf].SetY(params[:top])
690 710 params[:pdf].SetX(15)
691 711 char_limit = PDF::MaxCharactorsForSubject - params[:indent]
692 712 params[:pdf].RDMCell(params[:subject_width] - 15, 5,
693 713 (" " * params[:indent]) +
694 714 subject.to_s.sub(/^(.{#{char_limit}}[^\s]*\s).*$/, '\1 (...)'),
695 715 "LR")
696 716 params[:pdf].SetY(params[:top])
697 717 params[:pdf].SetX(params[:subject_width])
698 718 params[:pdf].RDMCell(params[:g_width], 5, "", "LR")
699 719 end
700 720
701 721 def image_subject(params, subject, options={})
702 722 params[:image].fill('black')
703 723 params[:image].stroke('transparent')
704 724 params[:image].stroke_width(1)
705 725 params[:image].text(params[:indent], params[:top] + 2, subject)
706 726 end
707 727
728 def issue_relations(issue)
729 rels = {}
730 if relations[issue.id]
731 relations[issue.id].each do |relation|
732 (rels[relation.relation_type] ||= []) << relation.issue_to_id
733 end
734 end
735 rels
736 end
737
708 738 def html_task(params, coords, options={})
709 739 output = ''
710 740 # Renders the task bar, with progress and late
711 741 if coords[:bar_start] && coords[:bar_end]
712 742 width = coords[:bar_end] - coords[:bar_start] - 2
713 743 style = ""
714 744 style << "top:#{params[:top]}px;"
715 745 style << "left:#{coords[:bar_start]}px;"
716 746 style << "width:#{width}px;"
717 output << view.content_tag(:div, '&nbsp;'.html_safe,
718 :style => style,
719 :class => "#{options[:css]} task_todo")
747 html_id = "task-todo-issue-#{options[:issue].id}" if options[:issue]
748 content_opt = {:style => style,
749 :class => "#{options[:css]} task_todo",
750 :id => html_id}
751 if options[:issue]
752 rels_hash = {}
753 issue_relations(options[:issue]).each do |k, v|
754 rels_hash[k] = v.join(',')
755 end
756 content_opt[:data] = {"rels" => rels_hash}
757 end
758 output << view.content_tag(:div, '&nbsp;'.html_safe, content_opt)
720 759 if coords[:bar_late_end]
721 760 width = coords[:bar_late_end] - coords[:bar_start] - 2
722 761 style = ""
723 762 style << "top:#{params[:top]}px;"
724 763 style << "left:#{coords[:bar_start]}px;"
725 764 style << "width:#{width}px;"
726 765 output << view.content_tag(:div, '&nbsp;'.html_safe,
727 766 :style => style,
728 767 :class => "#{options[:css]} task_late")
729 768 end
730 769 if coords[:bar_progress_end]
731 770 width = coords[:bar_progress_end] - coords[:bar_start] - 2
732 771 style = ""
733 772 style << "top:#{params[:top]}px;"
734 773 style << "left:#{coords[:bar_start]}px;"
735 774 style << "width:#{width}px;"
736 775 output << view.content_tag(:div, '&nbsp;'.html_safe,
737 776 :style => style,
738 777 :class => "#{options[:css]} task_done")
739 778 end
740 779 end
741 780 # Renders the markers
742 781 if options[:markers]
743 782 if coords[:start]
744 783 style = ""
745 784 style << "top:#{params[:top]}px;"
746 785 style << "left:#{coords[:start]}px;"
747 786 style << "width:15px;"
748 787 output << view.content_tag(:div, '&nbsp;'.html_safe,
749 788 :style => style,
750 789 :class => "#{options[:css]} marker starting")
751 790 end
752 791 if coords[:end]
753 792 style = ""
754 793 style << "top:#{params[:top]}px;"
755 794 style << "left:#{coords[:end] + params[:zoom]}px;"
756 795 style << "width:15px;"
757 796 output << view.content_tag(:div, '&nbsp;'.html_safe,
758 797 :style => style,
759 798 :class => "#{options[:css]} marker ending")
760 799 end
761 800 end
762 801 # Renders the label on the right
763 802 if options[:label]
764 803 style = ""
765 804 style << "top:#{params[:top]}px;"
766 805 style << "left:#{(coords[:bar_end] || 0) + 8}px;"
767 806 style << "width:15px;"
768 807 output << view.content_tag(:div, options[:label],
769 808 :style => style,
770 809 :class => "#{options[:css]} label")
771 810 end
772 811 # Renders the tooltip
773 812 if options[:issue] && coords[:bar_start] && coords[:bar_end]
774 813 s = view.content_tag(:span,
775 814 view.render_issue_tooltip(options[:issue]).html_safe,
776 815 :class => "tip")
777 816 style = ""
778 817 style << "position: absolute;"
779 818 style << "top:#{params[:top]}px;"
780 819 style << "left:#{coords[:bar_start]}px;"
781 820 style << "width:#{coords[:bar_end] - coords[:bar_start]}px;"
782 821 style << "height:12px;"
783 822 output << view.content_tag(:div, s.html_safe,
784 823 :style => style,
785 824 :class => "tooltip")
786 825 end
787 826 @lines << output
788 827 output
789 828 end
790 829
791 830 def pdf_task(params, coords, options={})
792 831 height = options[:height] || 2
793 832 # Renders the task bar, with progress and late
794 833 if coords[:bar_start] && coords[:bar_end]
795 834 params[:pdf].SetY(params[:top] + 1.5)
796 835 params[:pdf].SetX(params[:subject_width] + coords[:bar_start])
797 836 params[:pdf].SetFillColor(200, 200, 200)
798 837 params[:pdf].RDMCell(coords[:bar_end] - coords[:bar_start], height, "", 0, 0, "", 1)
799 838 if coords[:bar_late_end]
800 839 params[:pdf].SetY(params[:top] + 1.5)
801 840 params[:pdf].SetX(params[:subject_width] + coords[:bar_start])
802 841 params[:pdf].SetFillColor(255, 100, 100)
803 842 params[:pdf].RDMCell(coords[:bar_late_end] - coords[:bar_start], height, "", 0, 0, "", 1)
804 843 end
805 844 if coords[:bar_progress_end]
806 845 params[:pdf].SetY(params[:top] + 1.5)
807 846 params[:pdf].SetX(params[:subject_width] + coords[:bar_start])
808 847 params[:pdf].SetFillColor(90, 200, 90)
809 848 params[:pdf].RDMCell(coords[:bar_progress_end] - coords[:bar_start], height, "", 0, 0, "", 1)
810 849 end
811 850 end
812 851 # Renders the markers
813 852 if options[:markers]
814 853 if coords[:start]
815 854 params[:pdf].SetY(params[:top] + 1)
816 855 params[:pdf].SetX(params[:subject_width] + coords[:start] - 1)
817 856 params[:pdf].SetFillColor(50, 50, 200)
818 857 params[:pdf].RDMCell(2, 2, "", 0, 0, "", 1)
819 858 end
820 859 if coords[:end]
821 860 params[:pdf].SetY(params[:top] + 1)
822 861 params[:pdf].SetX(params[:subject_width] + coords[:end] - 1)
823 862 params[:pdf].SetFillColor(50, 50, 200)
824 863 params[:pdf].RDMCell(2, 2, "", 0, 0, "", 1)
825 864 end
826 865 end
827 866 # Renders the label on the right
828 867 if options[:label]
829 868 params[:pdf].SetX(params[:subject_width] + (coords[:bar_end] || 0) + 5)
830 869 params[:pdf].RDMCell(30, 2, options[:label])
831 870 end
832 871 end
833 872
834 873 def image_task(params, coords, options={})
835 874 height = options[:height] || 6
836 875 # Renders the task bar, with progress and late
837 876 if coords[:bar_start] && coords[:bar_end]
838 877 params[:image].fill('#aaa')
839 878 params[:image].rectangle(params[:subject_width] + coords[:bar_start],
840 879 params[:top],
841 880 params[:subject_width] + coords[:bar_end],
842 881 params[:top] - height)
843 882 if coords[:bar_late_end]
844 883 params[:image].fill('#f66')
845 884 params[:image].rectangle(params[:subject_width] + coords[:bar_start],
846 885 params[:top],
847 886 params[:subject_width] + coords[:bar_late_end],
848 887 params[:top] - height)
849 888 end
850 889 if coords[:bar_progress_end]
851 890 params[:image].fill('#00c600')
852 891 params[:image].rectangle(params[:subject_width] + coords[:bar_start],
853 892 params[:top],
854 893 params[:subject_width] + coords[:bar_progress_end],
855 894 params[:top] - height)
856 895 end
857 896 end
858 897 # Renders the markers
859 898 if options[:markers]
860 899 if coords[:start]
861 900 x = params[:subject_width] + coords[:start]
862 901 y = params[:top] - height / 2
863 902 params[:image].fill('blue')
864 903 params[:image].polygon(x - 4, y, x, y - 4, x + 4, y, x, y + 4)
865 904 end
866 905 if coords[:end]
867 906 x = params[:subject_width] + coords[:end] + params[:zoom]
868 907 y = params[:top] - height / 2
869 908 params[:image].fill('blue')
870 909 params[:image].polygon(x - 4, y, x, y - 4, x + 4, y, x, y + 4)
871 910 end
872 911 end
873 912 # Renders the label on the right
874 913 if options[:label]
875 914 params[:image].fill('black')
876 915 params[:image].text(params[:subject_width] + (coords[:bar_end] || 0) + 5,
877 916 params[:top] + 1,
878 917 options[:label])
879 918 end
880 919 end
881 920 end
882 921 end
883 922 end
@@ -1,107 +1,123
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../test_helper', __FILE__)
19 19
20 20 class GanttsControllerTest < ActionController::TestCase
21 21 fixtures :projects, :trackers, :issue_statuses, :issues,
22 22 :enumerations, :users, :issue_categories,
23 23 :projects_trackers,
24 24 :roles,
25 25 :member_roles,
26 26 :members,
27 27 :enabled_modules,
28 28 :workflows,
29 29 :versions
30 30
31 31 def test_gantt_should_work
32 32 i2 = Issue.find(2)
33 33 i2.update_attribute(:due_date, 1.month.from_now)
34 34 get :show, :project_id => 1
35 35 assert_response :success
36 36 assert_template 'gantts/show'
37 37 assert_not_nil assigns(:gantt)
38 38 # Issue with start and due dates
39 39 i = Issue.find(1)
40 40 assert_not_nil i.due_date
41 41 assert_select "div a.issue", /##{i.id}/
42 42 # Issue with on a targeted version should not be in the events but loaded in the html
43 43 i = Issue.find(2)
44 44 assert_select "div a.issue", /##{i.id}/
45 45 end
46 46
47 47 def test_gantt_should_work_without_issue_due_dates
48 48 Issue.update_all("due_date = NULL")
49 49 get :show, :project_id => 1
50 50 assert_response :success
51 51 assert_template 'gantts/show'
52 52 assert_not_nil assigns(:gantt)
53 53 end
54 54
55 55 def test_gantt_should_work_without_issue_and_version_due_dates
56 56 Issue.update_all("due_date = NULL")
57 57 Version.update_all("effective_date = NULL")
58 58 get :show, :project_id => 1
59 59 assert_response :success
60 60 assert_template 'gantts/show'
61 61 assert_not_nil assigns(:gantt)
62 62 end
63 63
64 64 def test_gantt_should_work_cross_project
65 65 get :show
66 66 assert_response :success
67 67 assert_template 'gantts/show'
68 68 assert_not_nil assigns(:gantt)
69 69 assert_not_nil assigns(:gantt).query
70 70 assert_nil assigns(:gantt).project
71 71 end
72 72
73 73 def test_gantt_should_not_disclose_private_projects
74 74 get :show
75 75 assert_response :success
76 76 assert_template 'gantts/show'
77 77 assert_tag 'a', :content => /eCookbook/
78 78 # Root private project
79 79 assert_no_tag 'a', {:content => /OnlineStore/}
80 80 # Private children of a public project
81 81 assert_no_tag 'a', :content => /Private child of eCookbook/
82 82 end
83 83
84 def test_gantt_should_display_relations
85 IssueRelation.delete_all
86 issue1 = Issue.generate!(:start_date => 1.day.from_now, :due_date => 3.day.from_now)
87 issue2 = Issue.generate!(:start_date => 1.day.from_now, :due_date => 3.day.from_now)
88 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, :relation_type => 'precedes')
89
90 get :show
91 assert_response :success
92
93 relations = assigns(:gantt).relations
94 assert_kind_of Hash, relations
95 assert relations.present?
96 assert_select 'div.task_todo[id=?][data-rels*=?]', "task-todo-issue-#{issue1.id}", issue2.id.to_s
97 assert_select 'div.task_todo[id=?][data-rels=?]', "task-todo-issue-#{issue2.id}", '{}'
98 end
99
84 100 def test_gantt_should_export_to_pdf
85 101 get :show, :project_id => 1, :format => 'pdf'
86 102 assert_response :success
87 103 assert_equal 'application/pdf', @response.content_type
88 104 assert @response.body.starts_with?('%PDF')
89 105 assert_not_nil assigns(:gantt)
90 106 end
91 107
92 108 def test_gantt_should_export_to_pdf_cross_project
93 109 get :show, :format => 'pdf'
94 110 assert_response :success
95 111 assert_equal 'application/pdf', @response.content_type
96 112 assert @response.body.starts_with?('%PDF')
97 113 assert_not_nil assigns(:gantt)
98 114 end
99 115
100 116 if Object.const_defined?(:Magick)
101 117 def test_gantt_should_export_to_png
102 118 get :show, :project_id => 1, :format => 'png'
103 119 assert_response :success
104 120 assert_equal 'image/png', @response.content_type
105 121 end
106 122 end
107 123 end
General Comments 0
You need to be logged in to leave comments. Login now