##// END OF EJS Templates
Fixed the zoom, previous, and next links on the Gantt chart....
Eric Davis -
r3960:5e1c29523003
parent child
Show More
@@ -1,266 +1,266
1 1 # redMine - project management software
2 2 # Copyright (C) 2006 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 IssuesHelper
19 19 include ApplicationHelper
20 20
21 21 def issue_list(issues, &block)
22 22 ancestors = []
23 23 issues.each do |issue|
24 24 while (ancestors.any? && !issue.is_descendant_of?(ancestors.last))
25 25 ancestors.pop
26 26 end
27 27 yield issue, ancestors.size
28 28 ancestors << issue unless issue.leaf?
29 29 end
30 30 end
31 31
32 32 def render_issue_tooltip(issue)
33 33 @cached_label_status ||= l(:field_status)
34 34 @cached_label_start_date ||= l(:field_start_date)
35 35 @cached_label_due_date ||= l(:field_due_date)
36 36 @cached_label_assigned_to ||= l(:field_assigned_to)
37 37 @cached_label_priority ||= l(:field_priority)
38 38 @cached_label_project ||= l(:field_project)
39 39
40 40 link_to_issue(issue) + "<br /><br />" +
41 41 "<strong>#{@cached_label_project}</strong>: #{link_to_project(issue.project)}<br />" +
42 42 "<strong>#{@cached_label_status}</strong>: #{issue.status.name}<br />" +
43 43 "<strong>#{@cached_label_start_date}</strong>: #{format_date(issue.start_date)}<br />" +
44 44 "<strong>#{@cached_label_due_date}</strong>: #{format_date(issue.due_date)}<br />" +
45 45 "<strong>#{@cached_label_assigned_to}</strong>: #{issue.assigned_to}<br />" +
46 46 "<strong>#{@cached_label_priority}</strong>: #{issue.priority.name}"
47 47 end
48 48
49 49 def render_issue_subject_with_tree(issue)
50 50 s = ''
51 51 issue.ancestors.each do |ancestor|
52 52 s << '<div>' + content_tag('p', link_to_issue(ancestor))
53 53 end
54 54 s << '<div>' + content_tag('h3', h(issue.subject))
55 55 s << '</div>' * (issue.ancestors.size + 1)
56 56 s
57 57 end
58 58
59 59 def render_descendants_tree(issue)
60 60 s = '<form><table class="list issues">'
61 61 issue_list(issue.descendants.sort_by(&:lft)) do |child, level|
62 62 s << content_tag('tr',
63 63 content_tag('td', check_box_tag("ids[]", child.id, false, :id => nil), :class => 'checkbox') +
64 64 content_tag('td', link_to_issue(child, :truncate => 60), :class => 'subject') +
65 65 content_tag('td', h(child.status)) +
66 66 content_tag('td', link_to_user(child.assigned_to)) +
67 67 content_tag('td', progress_bar(child.done_ratio, :width => '80px')),
68 68 :class => "issue issue-#{child.id} hascontextmenu #{level > 0 ? "idnt idnt-#{level}" : nil}")
69 69 end
70 70 s << '</form></table>'
71 71 s
72 72 end
73 73
74 74 def render_custom_fields_rows(issue)
75 75 return if issue.custom_field_values.empty?
76 76 ordered_values = []
77 77 half = (issue.custom_field_values.size / 2.0).ceil
78 78 half.times do |i|
79 79 ordered_values << issue.custom_field_values[i]
80 80 ordered_values << issue.custom_field_values[i + half]
81 81 end
82 82 s = "<tr>\n"
83 83 n = 0
84 84 ordered_values.compact.each do |value|
85 85 s << "</tr>\n<tr>\n" if n > 0 && (n % 2) == 0
86 86 s << "\t<th>#{ h(value.custom_field.name) }:</th><td>#{ simple_format_without_paragraph(h(show_value(value))) }</td>\n"
87 87 n += 1
88 88 end
89 89 s << "</tr>\n"
90 90 s
91 91 end
92 92
93 93 def sidebar_queries
94 94 unless @sidebar_queries
95 95 # User can see public queries and his own queries
96 96 visible = ARCondition.new(["is_public = ? OR user_id = ?", true, (User.current.logged? ? User.current.id : 0)])
97 97 # Project specific queries and global queries
98 98 visible << (@project.nil? ? ["project_id IS NULL"] : ["project_id IS NULL OR project_id = ?", @project.id])
99 99 @sidebar_queries = Query.find(:all,
100 100 :select => 'id, name',
101 101 :order => "name ASC",
102 102 :conditions => visible.conditions)
103 103 end
104 104 @sidebar_queries
105 105 end
106 106
107 107 def show_detail(detail, no_html=false)
108 108 case detail.property
109 109 when 'attr'
110 110 field = detail.prop_key.to_s.gsub(/\_id$/, "")
111 111 label = l(("field_" + field).to_sym)
112 112 case
113 113 when ['due_date', 'start_date'].include?(detail.prop_key)
114 114 value = format_date(detail.value.to_date) if detail.value
115 115 old_value = format_date(detail.old_value.to_date) if detail.old_value
116 116
117 117 when ['project_id', 'status_id', 'tracker_id', 'assigned_to_id', 'priority_id', 'category_id', 'fixed_version_id'].include?(detail.prop_key)
118 118 value = find_name_by_reflection(field, detail.value)
119 119 old_value = find_name_by_reflection(field, detail.old_value)
120 120
121 121 when detail.prop_key == 'estimated_hours'
122 122 value = "%0.02f" % detail.value.to_f unless detail.value.blank?
123 123 old_value = "%0.02f" % detail.old_value.to_f unless detail.old_value.blank?
124 124
125 125 when detail.prop_key == 'parent_id'
126 126 label = l(:field_parent_issue)
127 127 value = "##{detail.value}" unless detail.value.blank?
128 128 old_value = "##{detail.old_value}" unless detail.old_value.blank?
129 129 end
130 130 when 'cf'
131 131 custom_field = CustomField.find_by_id(detail.prop_key)
132 132 if custom_field
133 133 label = custom_field.name
134 134 value = format_value(detail.value, custom_field.field_format) if detail.value
135 135 old_value = format_value(detail.old_value, custom_field.field_format) if detail.old_value
136 136 end
137 137 when 'attachment'
138 138 label = l(:label_attachment)
139 139 end
140 140 call_hook(:helper_issues_show_detail_after_setting, {:detail => detail, :label => label, :value => value, :old_value => old_value })
141 141
142 142 label ||= detail.prop_key
143 143 value ||= detail.value
144 144 old_value ||= detail.old_value
145 145
146 146 unless no_html
147 147 label = content_tag('strong', label)
148 148 old_value = content_tag("i", h(old_value)) if detail.old_value
149 149 old_value = content_tag("strike", old_value) if detail.old_value and (!detail.value or detail.value.empty?)
150 150 if detail.property == 'attachment' && !value.blank? && a = Attachment.find_by_id(detail.prop_key)
151 151 # Link to the attachment if it has not been removed
152 152 value = link_to_attachment(a)
153 153 else
154 154 value = content_tag("i", h(value)) if value
155 155 end
156 156 end
157 157
158 158 if !detail.value.blank?
159 159 case detail.property
160 160 when 'attr', 'cf'
161 161 if !detail.old_value.blank?
162 162 l(:text_journal_changed, :label => label, :old => old_value, :new => value)
163 163 else
164 164 l(:text_journal_set_to, :label => label, :value => value)
165 165 end
166 166 when 'attachment'
167 167 l(:text_journal_added, :label => label, :value => value)
168 168 end
169 169 else
170 170 l(:text_journal_deleted, :label => label, :old => old_value)
171 171 end
172 172 end
173 173
174 174 # Find the name of an associated record stored in the field attribute
175 175 def find_name_by_reflection(field, id)
176 176 association = Issue.reflect_on_association(field.to_sym)
177 177 if association
178 178 record = association.class_name.constantize.find_by_id(id)
179 179 return record.name if record
180 180 end
181 181 end
182 182
183 183 def issues_to_csv(issues, project = nil)
184 184 ic = Iconv.new(l(:general_csv_encoding), 'UTF-8')
185 185 decimal_separator = l(:general_csv_decimal_separator)
186 186 export = FCSV.generate(:col_sep => l(:general_csv_separator)) do |csv|
187 187 # csv header fields
188 188 headers = [ "#",
189 189 l(:field_status),
190 190 l(:field_project),
191 191 l(:field_tracker),
192 192 l(:field_priority),
193 193 l(:field_subject),
194 194 l(:field_assigned_to),
195 195 l(:field_category),
196 196 l(:field_fixed_version),
197 197 l(:field_author),
198 198 l(:field_start_date),
199 199 l(:field_due_date),
200 200 l(:field_done_ratio),
201 201 l(:field_estimated_hours),
202 202 l(:field_parent_issue),
203 203 l(:field_created_on),
204 204 l(:field_updated_on)
205 205 ]
206 206 # Export project custom fields if project is given
207 207 # otherwise export custom fields marked as "For all projects"
208 208 custom_fields = project.nil? ? IssueCustomField.for_all : project.all_issue_custom_fields
209 209 custom_fields.each {|f| headers << f.name}
210 210 # Description in the last column
211 211 headers << l(:field_description)
212 212 csv << headers.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
213 213 # csv lines
214 214 issues.each do |issue|
215 215 fields = [issue.id,
216 216 issue.status.name,
217 217 issue.project.name,
218 218 issue.tracker.name,
219 219 issue.priority.name,
220 220 issue.subject,
221 221 issue.assigned_to,
222 222 issue.category,
223 223 issue.fixed_version,
224 224 issue.author.name,
225 225 format_date(issue.start_date),
226 226 format_date(issue.due_date),
227 227 issue.done_ratio,
228 228 issue.estimated_hours.to_s.gsub('.', decimal_separator),
229 229 issue.parent_id,
230 230 format_time(issue.created_on),
231 231 format_time(issue.updated_on)
232 232 ]
233 233 custom_fields.each {|f| fields << show_value(issue.custom_value_for(f)) }
234 234 fields << issue.description
235 235 csv << fields.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
236 236 end
237 237 end
238 238 export
239 239 end
240 240
241 241 def gantt_zoom_link(gantt, in_or_out)
242 242 img_attributes = {:style => 'height:1.4em; width:1.4em; margin-left: 3px;'} # em for accessibility
243 243
244 244 case in_or_out
245 245 when :in
246 246 if gantt.zoom < 4
247 247 link_to_remote(l(:text_zoom_in) + image_tag('zoom_in.png', img_attributes.merge(:alt => l(:text_zoom_in))),
248 {:url => gantt.params.merge(:zoom => (gantt.zoom+1)), :update => 'content'},
248 {:url => gantt.params.merge(:zoom => (gantt.zoom+1)), :method => :get, :update => 'content'},
249 249 {:href => url_for(gantt.params.merge(:zoom => (gantt.zoom+1)))})
250 250 else
251 251 l(:text_zoom_in) +
252 252 image_tag('zoom_in_g.png', img_attributes.merge(:alt => l(:text_zoom_in)))
253 253 end
254 254
255 255 when :out
256 256 if gantt.zoom > 1
257 257 link_to_remote(l(:text_zoom_out) + image_tag('zoom_out.png', img_attributes.merge(:alt => l(:text_zoom_out))),
258 {:url => gantt.params.merge(:zoom => (gantt.zoom-1)), :update => 'content'},
258 {:url => gantt.params.merge(:zoom => (gantt.zoom-1)), :method => :get, :update => 'content'},
259 259 {:href => url_for(gantt.params.merge(:zoom => (gantt.zoom-1)))})
260 260 else
261 261 l(:text_zoom_out) +
262 262 image_tag('zoom_out_g.png', img_attributes.merge(:alt => l(:text_zoom_out)))
263 263 end
264 264 end
265 265 end
266 266 end
@@ -1,187 +1,187
1 1 <% @gantt.view = self %>
2 2 <h2><%= l(:label_gantt) %></h2>
3 3
4 4 <% form_tag(gantt_path(:month => params[:month], :year => params[:year], :months => params[:months]), :method => :put, :id => 'query_form') do %>
5 5 <%= hidden_field_tag('project_id', @project.to_param) if @project%>
6 6 <fieldset id="filters" class="collapsible">
7 7 <legend onclick="toggleFieldset(this);"><%= l(:label_filter_plural) %></legend>
8 8 <div>
9 9 <%= render :partial => 'queries/filters', :locals => {:query => @query} %>
10 10 </div>
11 11 </fieldset>
12 12
13 13 <p style="float:right;">
14 14 <%= gantt_zoom_link(@gantt, :in) %>
15 15 <%= gantt_zoom_link(@gantt, :out) %>
16 16 </p>
17 17
18 18 <p class="buttons">
19 19 <%= text_field_tag 'months', @gantt.months, :size => 2 %>
20 20 <%= l(:label_months_from) %>
21 21 <%= select_month(@gantt.month_from, :prefix => "month", :discard_type => true) %>
22 22 <%= select_year(@gantt.year_from, :prefix => "year", :discard_type => true) %>
23 23 <%= hidden_field_tag 'zoom', @gantt.zoom %>
24 24
25 25 <%= link_to_remote l(:button_apply),
26 26 { :url => { :set_filter => (@query.new_record? ? 1 : nil) },
27 27 :update => "content",
28 28 :with => "Form.serialize('query_form')"
29 29 }, :class => 'icon icon-checked' %>
30 30
31 31 <%= link_to_remote l(:button_clear),
32 32 { :url => { :set_filter => (@query.new_record? ? 1 : nil) },
33 33 :update => "content",
34 34 }, :class => 'icon icon-reload' if @query.new_record? %>
35 35 </p>
36 36 <% end %>
37 37
38 38 <%= error_messages_for 'query' %>
39 39 <% if @query.valid? %>
40 40 <% zoom = 1
41 41 @gantt.zoom.times { zoom = zoom * 2 }
42 42
43 43 subject_width = 330
44 44 header_heigth = 18
45 45
46 46 headers_height = header_heigth
47 47 show_weeks = false
48 48 show_days = false
49 49
50 50 if @gantt.zoom >1
51 51 show_weeks = true
52 52 headers_height = 2*header_heigth
53 53 if @gantt.zoom > 2
54 54 show_days = true
55 55 headers_height = 3*header_heigth
56 56 end
57 57 end
58 58
59 59 # Width of the entire chart
60 60 g_width = (@gantt.date_to - @gantt.date_from + 1)*zoom
61 61 # Collect the number of issues on Versions
62 62 g_height = [(20 * (@gantt.number_of_rows + 6))+150, 206].max
63 63 t_height = g_height + headers_height
64 64 %>
65 65 <table width="100%" style="border:0; border-collapse: collapse;">
66 66 <tr>
67 67 <td style="width:<%= subject_width %>px; padding:0px;">
68 68
69 69 <div style="position:relative;height:<%= t_height + 24 %>px;width:<%= subject_width + 1 %>px;">
70 70 <div style="right:-2px;width:<%= subject_width %>px;height:<%= headers_height %>px;background: #eee;" class="gantt_hdr"></div>
71 71 <div style="right:-2px;width:<%= subject_width %>px;height:<%= t_height %>px;border-left: 1px solid #c0c0c0;overflow:hidden;" class="gantt_hdr"></div>
72 72 <% top = headers_height + 8 %>
73 73
74 74 <%= @gantt.subjects(:headers_height => headers_height, :top => top, :g_width => g_width) %>
75 75
76 76 </div>
77 77 </td>
78 78 <td>
79 79
80 80 <div style="position:relative;height:<%= t_height + 24 %>px;overflow:auto;">
81 81 <div style="width:<%= g_width-1 %>px;height:<%= headers_height %>px;background: #eee;" class="gantt_hdr">&nbsp;</div>
82 82 <%
83 83 #
84 84 # Months headers
85 85 #
86 86 month_f = @gantt.date_from
87 87 left = 0
88 88 height = (show_weeks ? header_heigth : header_heigth + g_height)
89 89 @gantt.months.times do
90 90 width = ((month_f >> 1) - month_f) * zoom - 1
91 91 %>
92 92 <div style="left:<%= left %>px;width:<%= width %>px;height:<%= height %>px;" class="gantt_hdr">
93 93 <%= link_to "#{month_f.year}-#{month_f.month}", @gantt.params.merge(:year => month_f.year, :month => month_f.month), :title => "#{month_name(month_f.month)} #{month_f.year}"%>
94 94 </div>
95 95 <%
96 96 left = left + width + 1
97 97 month_f = month_f >> 1
98 98 end %>
99 99
100 100 <%
101 101 #
102 102 # Weeks headers
103 103 #
104 104 if show_weeks
105 105 left = 0
106 106 height = (show_days ? header_heigth-1 : header_heigth-1 + g_height)
107 107 if @gantt.date_from.cwday == 1
108 108 # @date_from is monday
109 109 week_f = @gantt.date_from
110 110 else
111 111 # find next monday after @date_from
112 112 week_f = @gantt.date_from + (7 - @gantt.date_from.cwday + 1)
113 113 width = (7 - @gantt.date_from.cwday + 1) * zoom-1
114 114 %>
115 115 <div style="left:<%= left %>px;top:19px;width:<%= width %>px;height:<%= height %>px;" class="gantt_hdr">&nbsp;</div>
116 116 <%
117 117 left = left + width+1
118 118 end %>
119 119 <%
120 120 while week_f <= @gantt.date_to
121 121 width = (week_f + 6 <= @gantt.date_to) ? 7 * zoom -1 : (@gantt.date_to - week_f + 1) * zoom-1
122 122 %>
123 123 <div style="left:<%= left %>px;top:19px;width:<%= width %>px;height:<%= height %>px;" class="gantt_hdr">
124 124 <small><%= week_f.cweek if width >= 16 %></small>
125 125 </div>
126 126 <%
127 127 left = left + width+1
128 128 week_f = week_f+7
129 129 end
130 130 end %>
131 131
132 132 <%
133 133 #
134 134 # Days headers
135 135 #
136 136 if show_days
137 137 left = 0
138 138 height = g_height + header_heigth - 1
139 139 wday = @gantt.date_from.cwday
140 140 (@gantt.date_to - @gantt.date_from + 1).to_i.times do
141 141 width = zoom - 1
142 142 %>
143 143 <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">
144 144 <%= day_name(wday).first %>
145 145 </div>
146 146 <%
147 147 left = left + width+1
148 148 wday = wday + 1
149 149 wday = 1 if wday > 7
150 150 end
151 151 end %>
152 152
153 153 <% top = headers_height + 10 %>
154 154
155 155 <%= @gantt.lines(:top => top, :zoom => zoom, :g_width => g_width ) %>
156 156
157 157 <%
158 158 #
159 159 # Today red line (excluded from cache)
160 160 #
161 161 if Date.today >= @gantt.date_from and Date.today <= @gantt.date_to %>
162 162 <div style="position: absolute;height:<%= g_height %>px;top:<%= headers_height + 1 %>px;left:<%= ((Date.today-@gantt.date_from+1)*zoom).floor()-1 %>px;width:10px;border-left: 1px dashed red;">&nbsp;</div>
163 163 <% end %>
164 164
165 165 </div>
166 166 </td>
167 167 </tr>
168 168 </table>
169 169
170 170 <table width="100%">
171 171 <tr>
172 <td align="left"><%= link_to_remote ('&#171; ' + l(:label_previous)), {:url => @gantt.params_previous, :update => 'content', :complete => 'window.scrollTo(0,0)'}, {:href => url_for(@gantt.params_previous)} %></td>
173 <td align="right"><%= link_to_remote (l(:label_next) + ' &#187;'), {:url => @gantt.params_next, :update => 'content', :complete => 'window.scrollTo(0,0)'}, {:href => url_for(@gantt.params_next)} %></td>
172 <td align="left"><%= link_to_remote ('&#171; ' + l(:label_previous)), {:url => @gantt.params_previous, :method => :get, :update => 'content', :complete => 'window.scrollTo(0,0)'}, {:href => url_for(@gantt.params_previous)} %></td>
173 <td align="right"><%= link_to_remote (l(:label_next) + ' &#187;'), {:url => @gantt.params_next, :method => :get, :update => 'content', :complete => 'window.scrollTo(0,0)'}, {:href => url_for(@gantt.params_next)} %></td>
174 174 </tr>
175 175 </table>
176 176
177 177 <% other_formats_links do |f| %>
178 178 <%= f.link_to 'PDF', :url => @gantt.params %>
179 179 <%= f.link_to('PNG', :url => @gantt.params) if @gantt.respond_to?('to_image') %>
180 180 <% end %>
181 181 <% end # query.valid? %>
182 182
183 183 <% content_for :sidebar do %>
184 184 <%= render :partial => 'issues/sidebar' %>
185 185 <% end %>
186 186
187 187 <% html_title(l(:label_gantt)) -%>
@@ -1,972 +1,976
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2008 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
25 25 # :nodoc:
26 26 # Some utility methods for the PDF export
27 27 class PDF
28 28 MaxCharactorsForSubject = 45
29 29 TotalWidth = 280
30 30 LeftPaneWidth = 100
31 31
32 32 def self.right_pane_width
33 33 TotalWidth - LeftPaneWidth
34 34 end
35 35 end
36 36
37 37 attr_reader :year_from, :month_from, :date_from, :date_to, :zoom, :months
38 38 attr_accessor :query
39 39 attr_accessor :project
40 40 attr_accessor :view
41 41
42 42 def initialize(options={})
43 43 options = options.dup
44 44
45 45 if options[:year] && options[:year].to_i >0
46 46 @year_from = options[:year].to_i
47 47 if options[:month] && options[:month].to_i >=1 && options[:month].to_i <= 12
48 48 @month_from = options[:month].to_i
49 49 else
50 50 @month_from = 1
51 51 end
52 52 else
53 53 @month_from ||= Date.today.month
54 54 @year_from ||= Date.today.year
55 55 end
56 56
57 57 zoom = (options[:zoom] || User.current.pref[:gantt_zoom]).to_i
58 58 @zoom = (zoom > 0 && zoom < 5) ? zoom : 2
59 59 months = (options[:months] || User.current.pref[:gantt_months]).to_i
60 60 @months = (months > 0 && months < 25) ? months : 6
61 61
62 62 # Save gantt parameters as user preference (zoom and months count)
63 63 if (User.current.logged? && (@zoom != User.current.pref[:gantt_zoom] || @months != User.current.pref[:gantt_months]))
64 64 User.current.pref[:gantt_zoom], User.current.pref[:gantt_months] = @zoom, @months
65 65 User.current.preference.save
66 66 end
67 67
68 68 @date_from = Date.civil(@year_from, @month_from, 1)
69 69 @date_to = (@date_from >> @months) - 1
70 70 end
71
72 def common_params
73 { :controller => 'gantts', :action => 'show', :project_id => @project }
74 end
71 75
72 76 def params
73 { :zoom => zoom, :year => year_from, :month => month_from, :months => months }
77 common_params.merge({ :zoom => zoom, :year => year_from, :month => month_from, :months => months })
74 78 end
75 79
76 80 def params_previous
77 { :year => (date_from << months).year, :month => (date_from << months).month, :zoom => zoom, :months => months }
81 common_params.merge({:year => (date_from << months).year, :month => (date_from << months).month, :zoom => zoom, :months => months })
78 82 end
79 83
80 84 def params_next
81 { :year => (date_from >> months).year, :month => (date_from >> months).month, :zoom => zoom, :months => months }
85 common_params.merge({:year => (date_from >> months).year, :month => (date_from >> months).month, :zoom => zoom, :months => months })
82 86 end
83 87
84 88 ### Extracted from the HTML view/helpers
85 89 # Returns the number of rows that will be rendered on the Gantt chart
86 90 def number_of_rows
87 91 if @project
88 92 return number_of_rows_on_project(@project)
89 93 else
90 94 Project.roots.inject(0) do |total, project|
91 95 total += number_of_rows_on_project(project)
92 96 end
93 97 end
94 98 end
95 99
96 100 # Returns the number of rows that will be used to list a project on
97 101 # the Gantt chart. This will recurse for each subproject.
98 102 def number_of_rows_on_project(project)
99 103 # Remove the project requirement for Versions because it will
100 104 # restrict issues to only be on the current project. This
101 105 # ends up missing issues which are assigned to shared versions.
102 106 @query.project = nil if @query.project
103 107
104 108 # One Root project
105 109 count = 1
106 110 # Issues without a Version
107 111 count += project.issues.for_gantt.without_version.with_query(@query).count
108 112
109 113 # Versions
110 114 count += project.versions.count
111 115
112 116 # Issues on the Versions
113 117 project.versions.each do |version|
114 118 count += version.fixed_issues.for_gantt.with_query(@query).count
115 119 end
116 120
117 121 # Subprojects
118 122 project.children.each do |subproject|
119 123 count += number_of_rows_on_project(subproject)
120 124 end
121 125
122 126 count
123 127 end
124 128
125 129 # Renders the subjects of the Gantt chart, the left side.
126 130 def subjects(options={})
127 131 options = {:indent => 4, :render => :subject, :format => :html}.merge(options)
128 132
129 133 output = ''
130 134 if @project
131 135 output << render_project(@project, options)
132 136 else
133 137 Project.roots.each do |project|
134 138 output << render_project(project, options)
135 139 end
136 140 end
137 141
138 142 output
139 143 end
140 144
141 145 # Renders the lines of the Gantt chart, the right side
142 146 def lines(options={})
143 147 options = {:indent => 4, :render => :line, :format => :html}.merge(options)
144 148 output = ''
145 149
146 150 if @project
147 151 output << render_project(@project, options)
148 152 else
149 153 Project.roots.each do |project|
150 154 output << render_project(project, options)
151 155 end
152 156 end
153 157
154 158 output
155 159 end
156 160
157 161 def render_project(project, options={})
158 162 options[:top] = 0 unless options.key? :top
159 163 options[:indent_increment] = 20 unless options.key? :indent_increment
160 164 options[:top_increment] = 20 unless options.key? :top_increment
161 165
162 166 output = ''
163 167 # Project Header
164 168 project_header = if options[:render] == :subject
165 169 subject_for_project(project, options)
166 170 else
167 171 # :line
168 172 line_for_project(project, options)
169 173 end
170 174 output << project_header if options[:format] == :html
171 175
172 176 options[:top] += options[:top_increment]
173 177 options[:indent] += options[:indent_increment]
174 178
175 179 # Second, Issues without a version
176 180 issues = project.issues.for_gantt.without_version.with_query(@query)
177 181 if issues
178 182 issue_rendering = render_issues(issues, options)
179 183 output << issue_rendering if options[:format] == :html
180 184 end
181 185
182 186 # Third, Versions
183 187 project.versions.sort.each do |version|
184 188 version_rendering = render_version(version, options)
185 189 output << version_rendering if options[:format] == :html
186 190 end
187 191
188 192 # Fourth, subprojects
189 193 project.children.each do |project|
190 194 subproject_rendering = render_project(project, options)
191 195 output << subproject_rendering if options[:format] == :html
192 196 end
193 197
194 198 # Remove indent to hit the next sibling
195 199 options[:indent] -= options[:indent_increment]
196 200
197 201 output
198 202 end
199 203
200 204 def render_issues(issues, options={})
201 205 output = ''
202 206 issues.each do |i|
203 207 issue_rendering = if options[:render] == :subject
204 208 subject_for_issue(i, options)
205 209 else
206 210 # :line
207 211 line_for_issue(i, options)
208 212 end
209 213 output << issue_rendering if options[:format] == :html
210 214 options[:top] += options[:top_increment]
211 215 end
212 216 output
213 217 end
214 218
215 219 def render_version(version, options={})
216 220 output = ''
217 221 # Version header
218 222 version_rendering = if options[:render] == :subject
219 223 subject_for_version(version, options)
220 224 else
221 225 # :line
222 226 line_for_version(version, options)
223 227 end
224 228
225 229 output << version_rendering if options[:format] == :html
226 230
227 231 options[:top] += options[:top_increment]
228 232
229 233 # Remove the project requirement for Versions because it will
230 234 # restrict issues to only be on the current project. This
231 235 # ends up missing issues which are assigned to shared versions.
232 236 @query.project = nil if @query.project
233 237
234 238 issues = version.fixed_issues.for_gantt.with_query(@query)
235 239 if issues
236 240 # Indent issues
237 241 options[:indent] += options[:indent_increment]
238 242 output << render_issues(issues, options)
239 243 options[:indent] -= options[:indent_increment]
240 244 end
241 245
242 246 output
243 247 end
244 248
245 249 def subject_for_project(project, options)
246 250 case options[:format]
247 251 when :html
248 252 output = ''
249 253
250 254 output << "<div class='project-name' style='position: absolute;line-height:1.2em;height:16px;top:#{options[:top]}px;left:#{options[:indent]}px;overflow:hidden;'><small> "
251 255 if project.is_a? Project
252 256 output << "<span class='icon icon-projects #{project.overdue? ? 'project-overdue' : ''}'>"
253 257 output << view.link_to_project(project)
254 258 output << '</span>'
255 259 else
256 260 ActiveRecord::Base.logger.debug "Gantt#subject_for_project was not given a project"
257 261 ''
258 262 end
259 263 output << "</small></div>"
260 264
261 265 output
262 266 when :image
263 267
264 268 options[:image].fill('black')
265 269 options[:image].stroke('transparent')
266 270 options[:image].stroke_width(1)
267 271 options[:image].text(options[:indent], options[:top] + 2, project.name)
268 272 when :pdf
269 273 options[:pdf].SetY(options[:top])
270 274 options[:pdf].SetX(15)
271 275
272 276 char_limit = PDF::MaxCharactorsForSubject - options[:indent]
273 277 options[:pdf].Cell(options[:subject_width]-15, 5, (" " * options[:indent]) +"#{project.name}".sub(/^(.{#{char_limit}}[^\s]*\s).*$/, '\1 (...)'), "LR")
274 278
275 279 options[:pdf].SetY(options[:top])
276 280 options[:pdf].SetX(options[:subject_width])
277 281 options[:pdf].Cell(options[:g_width], 5, "", "LR")
278 282 end
279 283 end
280 284
281 285 def line_for_project(project, options)
282 286 # Skip versions that don't have a start_date
283 287 if project.is_a?(Project) && project.start_date
284 288 options[:zoom] ||= 1
285 289 options[:g_width] ||= (self.date_to - self.date_from + 1) * options[:zoom]
286 290
287 291
288 292 case options[:format]
289 293 when :html
290 294 output = ''
291 295 i_left = ((project.start_date - self.date_from)*options[:zoom]).floor
292 296
293 297 start_date = project.start_date
294 298 start_date ||= self.date_from
295 299 start_left = ((start_date - self.date_from)*options[:zoom]).floor
296 300
297 301 i_end_date = ((project.due_date <= self.date_to) ? project.due_date : self.date_to )
298 302 i_done_date = start_date + ((project.due_date - start_date+1)* project.completed_percent(:include_subprojects => true)/100).floor
299 303 i_done_date = (i_done_date <= self.date_from ? self.date_from : i_done_date )
300 304 i_done_date = (i_done_date >= self.date_to ? self.date_to : i_done_date )
301 305
302 306 i_late_date = [i_end_date, Date.today].min if start_date < Date.today
303 307 i_end = ((i_end_date - self.date_from) * options[:zoom]).floor
304 308
305 309 i_width = (i_end - i_left + 1).floor - 2 # total width of the issue (- 2 for left and right borders)
306 310 d_width = ((i_done_date - start_date)*options[:zoom]).floor - 2 # done width
307 311 l_width = i_late_date ? ((i_late_date - start_date+1)*options[:zoom]).floor - 2 : 0 # delay width
308 312
309 313 # Bar graphic
310 314
311 315 # Make sure that negative i_left and i_width don't
312 316 # overflow the subject
313 317 if i_end > 0 && i_left <= options[:g_width]
314 318 output << "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:#{ i_width }px;' class='task project_todo'>&nbsp;</div>"
315 319 end
316 320
317 321 if l_width > 0 && i_left <= options[:g_width]
318 322 output << "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:#{ l_width }px;' class='task project_late'>&nbsp;</div>"
319 323 end
320 324 if d_width > 0 && i_left <= options[:g_width]
321 325 output<< "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:#{ d_width }px;' class='task project_done'>&nbsp;</div>"
322 326 end
323 327
324 328
325 329 # Starting diamond
326 330 if start_left <= options[:g_width] && start_left > 0
327 331 output << "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:15px;' class='task project-line starting'>&nbsp;</div>"
328 332 output << "<div style='top:#{ options[:top] }px;left:#{ start_left + 12 }px;' class='task label'>"
329 333 output << "</div>"
330 334 end
331 335
332 336 # Ending diamond
333 337 # Don't show items too far ahead
334 338 if i_end <= options[:g_width] && i_end > 0
335 339 output << "<div style='top:#{ options[:top] }px;left:#{ i_end }px;width:15px;' class='task project-line ending'>&nbsp;</div>"
336 340 end
337 341
338 342 # DIsplay the Project name and %
339 343 if i_end <= options[:g_width]
340 344 # Display the status even if it's floated off to the left
341 345 status_px = i_end + 12 # 12px for the diamond
342 346 status_px = 0 if status_px <= 0
343 347
344 348 output << "<div style='top:#{ options[:top] }px;left:#{ status_px }px;' class='task label project-name'>"
345 349 output << "<strong>#{h project } #{h project.completed_percent(:include_subprojects => true).to_i.to_s}%</strong>"
346 350 output << "</div>"
347 351 end
348 352
349 353 output
350 354 when :image
351 355 options[:image].stroke('transparent')
352 356 i_left = options[:subject_width] + ((project.due_date - self.date_from)*options[:zoom]).floor
353 357
354 358 # Make sure negative i_left doesn't overflow the subject
355 359 if i_left > options[:subject_width]
356 360 options[:image].fill('blue')
357 361 options[:image].rectangle(i_left, options[:top], i_left + 6, options[:top] - 6)
358 362 options[:image].fill('black')
359 363 options[:image].text(i_left + 11, options[:top] + 1, project.name)
360 364 end
361 365 when :pdf
362 366 options[:pdf].SetY(options[:top]+1.5)
363 367 i_left = ((project.due_date - @date_from)*options[:zoom])
364 368
365 369 # Make sure negative i_left doesn't overflow the subject
366 370 if i_left > 0
367 371 options[:pdf].SetX(options[:subject_width] + i_left)
368 372 options[:pdf].SetFillColor(50,50,200)
369 373 options[:pdf].Cell(2, 2, "", 0, 0, "", 1)
370 374
371 375 options[:pdf].SetY(options[:top]+1.5)
372 376 options[:pdf].SetX(options[:subject_width] + i_left + 3)
373 377 options[:pdf].Cell(30, 2, "#{project.name}")
374 378 end
375 379 end
376 380 else
377 381 ActiveRecord::Base.logger.debug "Gantt#line_for_project was not given a project with a start_date"
378 382 ''
379 383 end
380 384 end
381 385
382 386 def subject_for_version(version, options)
383 387 case options[:format]
384 388 when :html
385 389 output = ''
386 390 output << "<div class='version-name' style='position: absolute;line-height:1.2em;height:16px;top:#{options[:top]}px;left:#{options[:indent]}px;overflow:hidden;'><small> "
387 391 if version.is_a? Version
388 392 output << "<span class='icon icon-package #{version.behind_schedule? ? 'version-behind-schedule' : ''} #{version.overdue? ? 'version-overdue' : ''}'>"
389 393 output << view.link_to_version(version)
390 394 output << '</span>'
391 395 else
392 396 ActiveRecord::Base.logger.debug "Gantt#subject_for_version was not given a version"
393 397 ''
394 398 end
395 399 output << "</small></div>"
396 400
397 401 output
398 402 when :image
399 403 options[:image].fill('black')
400 404 options[:image].stroke('transparent')
401 405 options[:image].stroke_width(1)
402 406 options[:image].text(options[:indent], options[:top] + 2, version.to_s_with_project)
403 407 when :pdf
404 408 options[:pdf].SetY(options[:top])
405 409 options[:pdf].SetX(15)
406 410
407 411 char_limit = PDF::MaxCharactorsForSubject - options[:indent]
408 412 options[:pdf].Cell(options[:subject_width]-15, 5, (" " * options[:indent]) +"#{version.to_s_with_project}".sub(/^(.{#{char_limit}}[^\s]*\s).*$/, '\1 (...)'), "LR")
409 413
410 414 options[:pdf].SetY(options[:top])
411 415 options[:pdf].SetX(options[:subject_width])
412 416 options[:pdf].Cell(options[:g_width], 5, "", "LR")
413 417 end
414 418 end
415 419
416 420 def line_for_version(version, options)
417 421 # Skip versions that don't have a start_date
418 422 if version.is_a?(Version) && version.start_date
419 423 options[:zoom] ||= 1
420 424 options[:g_width] ||= (self.date_to - self.date_from + 1) * options[:zoom]
421 425
422 426 case options[:format]
423 427 when :html
424 428 output = ''
425 429 i_left = ((version.start_date - self.date_from)*options[:zoom]).floor
426 430 # TODO: or version.fixed_issues.collect(&:start_date).min
427 431 start_date = version.fixed_issues.minimum('start_date') if version.fixed_issues.present?
428 432 start_date ||= self.date_from
429 433 start_left = ((start_date - self.date_from)*options[:zoom]).floor
430 434
431 435 i_end_date = ((version.due_date <= self.date_to) ? version.due_date : self.date_to )
432 436 i_done_date = start_date + ((version.due_date - start_date+1)* version.completed_pourcent/100).floor
433 437 i_done_date = (i_done_date <= self.date_from ? self.date_from : i_done_date )
434 438 i_done_date = (i_done_date >= self.date_to ? self.date_to : i_done_date )
435 439
436 440 i_late_date = [i_end_date, Date.today].min if start_date < Date.today
437 441
438 442 i_width = (i_left - start_left + 1).floor - 2 # total width of the issue (- 2 for left and right borders)
439 443 d_width = ((i_done_date - start_date)*options[:zoom]).floor - 2 # done width
440 444 l_width = i_late_date ? ((i_late_date - start_date+1)*options[:zoom]).floor - 2 : 0 # delay width
441 445
442 446 i_end = ((i_end_date - self.date_from) * options[:zoom]).floor # Ending pixel
443 447
444 448 # Bar graphic
445 449
446 450 # Make sure that negative i_left and i_width don't
447 451 # overflow the subject
448 452 if i_width > 0 && i_left <= options[:g_width]
449 453 output << "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:#{ i_width }px;' class='task milestone_todo'>&nbsp;</div>"
450 454 end
451 455 if l_width > 0 && i_left <= options[:g_width]
452 456 output << "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:#{ l_width }px;' class='task milestone_late'>&nbsp;</div>"
453 457 end
454 458 if d_width > 0 && i_left <= options[:g_width]
455 459 output<< "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:#{ d_width }px;' class='task milestone_done'>&nbsp;</div>"
456 460 end
457 461
458 462
459 463 # Starting diamond
460 464 if start_left <= options[:g_width] && start_left > 0
461 465 output << "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:15px;' class='task milestone starting'>&nbsp;</div>"
462 466 output << "<div style='top:#{ options[:top] }px;left:#{ start_left + 12 }px;background:#fff;' class='task'>"
463 467 output << "</div>"
464 468 end
465 469
466 470 # Ending diamond
467 471 # Don't show items too far ahead
468 472 if i_left <= options[:g_width] && i_end > 0
469 473 output << "<div style='top:#{ options[:top] }px;left:#{ i_end }px;width:15px;' class='task milestone ending'>&nbsp;</div>"
470 474 end
471 475
472 476 # Display the Version name and %
473 477 if i_end <= options[:g_width]
474 478 # Display the status even if it's floated off to the left
475 479 status_px = i_end + 12 # 12px for the diamond
476 480 status_px = 0 if status_px <= 0
477 481
478 482 output << "<div style='top:#{ options[:top] }px;left:#{ status_px }px;' class='task label version-name'>"
479 483 output << h("#{version.project} -") unless @project && @project == version.project
480 484 output << "<strong>#{h version } #{h version.completed_pourcent.to_i.to_s}%</strong>"
481 485 output << "</div>"
482 486 end
483 487
484 488 output
485 489 when :image
486 490 options[:image].stroke('transparent')
487 491 i_left = options[:subject_width] + ((version.start_date - @date_from)*options[:zoom]).floor
488 492
489 493 # Make sure negative i_left doesn't overflow the subject
490 494 if i_left > options[:subject_width]
491 495 options[:image].fill('green')
492 496 options[:image].rectangle(i_left, options[:top], i_left + 6, options[:top] - 6)
493 497 options[:image].fill('black')
494 498 options[:image].text(i_left + 11, options[:top] + 1, version.name)
495 499 end
496 500 when :pdf
497 501 options[:pdf].SetY(options[:top]+1.5)
498 502 i_left = ((version.start_date - @date_from)*options[:zoom])
499 503
500 504 # Make sure negative i_left doesn't overflow the subject
501 505 if i_left > 0
502 506 options[:pdf].SetX(options[:subject_width] + i_left)
503 507 options[:pdf].SetFillColor(50,200,50)
504 508 options[:pdf].Cell(2, 2, "", 0, 0, "", 1)
505 509
506 510 options[:pdf].SetY(options[:top]+1.5)
507 511 options[:pdf].SetX(options[:subject_width] + i_left + 3)
508 512 options[:pdf].Cell(30, 2, "#{version.name}")
509 513 end
510 514 end
511 515 else
512 516 ActiveRecord::Base.logger.debug "Gantt#line_for_version was not given a version with a start_date"
513 517 ''
514 518 end
515 519 end
516 520
517 521 def subject_for_issue(issue, options)
518 522 case options[:format]
519 523 when :html
520 524 output = ''
521 525 output << "<div class='tooltip'>"
522 526 output << "<div class='issue-subject' style='position: absolute;line-height:1.2em;height:16px;top:#{options[:top]}px;left:#{options[:indent]}px;overflow:hidden;'><small> "
523 527 if issue.is_a? Issue
524 528 css_classes = []
525 529 css_classes << 'issue-overdue' if issue.overdue?
526 530 css_classes << 'issue-behind-schedule' if issue.behind_schedule?
527 531 css_classes << 'icon icon-issue' unless Setting.gravatar_enabled? && issue.assigned_to
528 532
529 533 if issue.assigned_to.present?
530 534 assigned_string = l(:field_assigned_to) + ": " + issue.assigned_to.name
531 535 output << view.avatar(issue.assigned_to, :class => 'gravatar icon-gravatar', :size => 10, :title => assigned_string)
532 536 end
533 537 output << "<span class='#{css_classes.join(' ')}'>"
534 538 output << view.link_to_issue(issue)
535 539 output << ":"
536 540 output << h(issue.subject)
537 541 output << '</span>'
538 542 else
539 543 ActiveRecord::Base.logger.debug "Gantt#subject_for_issue was not given an issue"
540 544 ''
541 545 end
542 546 output << "</small></div>"
543 547
544 548 # Tooltip
545 549 if issue.is_a? Issue
546 550 output << "<span class='tip' style='position: absolute;top:#{ options[:top].to_i + 16 }px;left:#{ options[:indent].to_i + 20 }px;'>"
547 551 output << view.render_issue_tooltip(issue)
548 552 output << "</span>"
549 553 end
550 554
551 555 output << "</div>"
552 556 output
553 557 when :image
554 558 options[:image].fill('black')
555 559 options[:image].stroke('transparent')
556 560 options[:image].stroke_width(1)
557 561 options[:image].text(options[:indent], options[:top] + 2, issue.subject)
558 562 when :pdf
559 563 options[:pdf].SetY(options[:top])
560 564 options[:pdf].SetX(15)
561 565
562 566 char_limit = PDF::MaxCharactorsForSubject - options[:indent]
563 567 options[:pdf].Cell(options[:subject_width]-15, 5, (" " * options[:indent]) +"#{issue.tracker} #{issue.id}: #{issue.subject}".sub(/^(.{#{char_limit}}[^\s]*\s).*$/, '\1 (...)'), "LR")
564 568
565 569 options[:pdf].SetY(options[:top])
566 570 options[:pdf].SetX(options[:subject_width])
567 571 options[:pdf].Cell(options[:g_width], 5, "", "LR")
568 572 end
569 573 end
570 574
571 575 def line_for_issue(issue, options)
572 576 # Skip issues that don't have a due_before (due_date or version's due_date)
573 577 if issue.is_a?(Issue) && issue.due_before
574 578 case options[:format]
575 579 when :html
576 580 output = ''
577 581 # Handle nil start_dates, rare but can happen.
578 582 i_start_date = if issue.start_date && issue.start_date >= self.date_from
579 583 issue.start_date
580 584 else
581 585 self.date_from
582 586 end
583 587
584 588 i_end_date = ((issue.due_before && issue.due_before <= self.date_to) ? issue.due_before : self.date_to )
585 589 i_done_date = i_start_date + ((issue.due_before - i_start_date+1)*issue.done_ratio/100).floor
586 590 i_done_date = (i_done_date <= self.date_from ? self.date_from : i_done_date )
587 591 i_done_date = (i_done_date >= self.date_to ? self.date_to : i_done_date )
588 592
589 593 i_late_date = [i_end_date, Date.today].min if i_start_date < Date.today
590 594
591 595 i_left = ((i_start_date - self.date_from)*options[:zoom]).floor
592 596 i_width = ((i_end_date - i_start_date + 1)*options[:zoom]).floor - 2 # total width of the issue (- 2 for left and right borders)
593 597 d_width = ((i_done_date - i_start_date)*options[:zoom]).floor - 2 # done width
594 598 l_width = i_late_date ? ((i_late_date - i_start_date+1)*options[:zoom]).floor - 2 : 0 # delay width
595 599 css = "task " + (issue.leaf? ? 'leaf' : 'parent')
596 600
597 601 # Make sure that negative i_left and i_width don't
598 602 # overflow the subject
599 603 if i_width > 0
600 604 output << "<div style='top:#{ options[:top] }px;left:#{ i_left }px;width:#{ i_width }px;' class='#{css} task_todo'>&nbsp;</div>"
601 605 end
602 606 if l_width > 0
603 607 output << "<div style='top:#{ options[:top] }px;left:#{ i_left }px;width:#{ l_width }px;' class='#{css} task_late'>&nbsp;</div>"
604 608 end
605 609 if d_width > 0
606 610 output<< "<div style='top:#{ options[:top] }px;left:#{ i_left }px;width:#{ d_width }px;' class='#{css} task_done'>&nbsp;</div>"
607 611 end
608 612
609 613 # Display the status even if it's floated off to the left
610 614 status_px = i_left + i_width + 5
611 615 status_px = 5 if status_px <= 0
612 616
613 617 output << "<div style='top:#{ options[:top] }px;left:#{ status_px }px;' class='#{css} label issue-name'>"
614 618 output << issue.status.name
615 619 output << ' '
616 620 output << (issue.done_ratio).to_i.to_s
617 621 output << "%"
618 622 output << "</div>"
619 623
620 624 output << "<div class='tooltip' style='position: absolute;top:#{ options[:top] }px;left:#{ i_left }px;width:#{ i_width }px;height:12px;'>"
621 625 output << '<span class="tip">'
622 626 output << view.render_issue_tooltip(issue)
623 627 output << "</span></div>"
624 628 output
625 629
626 630 when :image
627 631 # Handle nil start_dates, rare but can happen.
628 632 i_start_date = if issue.start_date && issue.start_date >= @date_from
629 633 issue.start_date
630 634 else
631 635 @date_from
632 636 end
633 637
634 638 i_end_date = (issue.due_before <= date_to ? issue.due_before : date_to )
635 639 i_done_date = i_start_date + ((issue.due_before - i_start_date+1)*issue.done_ratio/100).floor
636 640 i_done_date = (i_done_date <= @date_from ? @date_from : i_done_date )
637 641 i_done_date = (i_done_date >= date_to ? date_to : i_done_date )
638 642 i_late_date = [i_end_date, Date.today].min if i_start_date < Date.today
639 643
640 644 i_left = options[:subject_width] + ((i_start_date - @date_from)*options[:zoom]).floor
641 645 i_width = ((i_end_date - i_start_date + 1)*options[:zoom]).floor # total width of the issue
642 646 d_width = ((i_done_date - i_start_date)*options[:zoom]).floor # done width
643 647 l_width = i_late_date ? ((i_late_date - i_start_date+1)*options[:zoom]).floor : 0 # delay width
644 648
645 649
646 650 # Make sure that negative i_left and i_width don't
647 651 # overflow the subject
648 652 if i_width > 0
649 653 options[:image].fill('grey')
650 654 options[:image].rectangle(i_left, options[:top], i_left + i_width, options[:top] - 6)
651 655 options[:image].fill('red')
652 656 options[:image].rectangle(i_left, options[:top], i_left + l_width, options[:top] - 6) if l_width > 0
653 657 options[:image].fill('blue')
654 658 options[:image].rectangle(i_left, options[:top], i_left + d_width, options[:top] - 6) if d_width > 0
655 659 end
656 660
657 661 # Show the status and % done next to the subject if it overflows
658 662 options[:image].fill('black')
659 663 if i_width > 0
660 664 options[:image].text(i_left + i_width + 5,options[:top] + 1, "#{issue.status.name} #{issue.done_ratio}%")
661 665 else
662 666 options[:image].text(options[:subject_width] + 5,options[:top] + 1, "#{issue.status.name} #{issue.done_ratio}%")
663 667 end
664 668
665 669 when :pdf
666 670 options[:pdf].SetY(options[:top]+1.5)
667 671 # Handle nil start_dates, rare but can happen.
668 672 i_start_date = if issue.start_date && issue.start_date >= @date_from
669 673 issue.start_date
670 674 else
671 675 @date_from
672 676 end
673 677
674 678 i_end_date = (issue.due_before <= @date_to ? issue.due_before : @date_to )
675 679
676 680 i_done_date = i_start_date + ((issue.due_before - i_start_date+1)*issue.done_ratio/100).floor
677 681 i_done_date = (i_done_date <= @date_from ? @date_from : i_done_date )
678 682 i_done_date = (i_done_date >= @date_to ? @date_to : i_done_date )
679 683
680 684 i_late_date = [i_end_date, Date.today].min if i_start_date < Date.today
681 685
682 686 i_left = ((i_start_date - @date_from)*options[:zoom])
683 687 i_width = ((i_end_date - i_start_date + 1)*options[:zoom])
684 688 d_width = ((i_done_date - i_start_date)*options[:zoom])
685 689 l_width = ((i_late_date - i_start_date+1)*options[:zoom]) if i_late_date
686 690 l_width ||= 0
687 691
688 692 # Make sure that negative i_left and i_width don't
689 693 # overflow the subject
690 694 if i_width > 0
691 695 options[:pdf].SetX(options[:subject_width] + i_left)
692 696 options[:pdf].SetFillColor(200,200,200)
693 697 options[:pdf].Cell(i_width, 2, "", 0, 0, "", 1)
694 698 end
695 699
696 700 if l_width > 0
697 701 options[:pdf].SetY(options[:top]+1.5)
698 702 options[:pdf].SetX(options[:subject_width] + i_left)
699 703 options[:pdf].SetFillColor(255,100,100)
700 704 options[:pdf].Cell(l_width, 2, "", 0, 0, "", 1)
701 705 end
702 706 if d_width > 0
703 707 options[:pdf].SetY(options[:top]+1.5)
704 708 options[:pdf].SetX(options[:subject_width] + i_left)
705 709 options[:pdf].SetFillColor(100,100,255)
706 710 options[:pdf].Cell(d_width, 2, "", 0, 0, "", 1)
707 711 end
708 712
709 713 options[:pdf].SetY(options[:top]+1.5)
710 714
711 715 # Make sure that negative i_left and i_width don't
712 716 # overflow the subject
713 717 if (i_left + i_width) >= 0
714 718 options[:pdf].SetX(options[:subject_width] + i_left + i_width)
715 719 else
716 720 options[:pdf].SetX(options[:subject_width])
717 721 end
718 722 options[:pdf].Cell(30, 2, "#{issue.status} #{issue.done_ratio}%")
719 723 end
720 724 else
721 725 ActiveRecord::Base.logger.debug "GanttHelper#line_for_issue was not given an issue with a due_before"
722 726 ''
723 727 end
724 728 end
725 729
726 730 # Generates a gantt image
727 731 # Only defined if RMagick is avalaible
728 732 def to_image(format='PNG')
729 733 date_to = (@date_from >> @months)-1
730 734 show_weeks = @zoom > 1
731 735 show_days = @zoom > 2
732 736
733 737 subject_width = 400
734 738 header_heigth = 18
735 739 # width of one day in pixels
736 740 zoom = @zoom*2
737 741 g_width = (@date_to - @date_from + 1)*zoom
738 742 g_height = 20 * number_of_rows + 30
739 743 headers_heigth = (show_weeks ? 2*header_heigth : header_heigth)
740 744 height = g_height + headers_heigth
741 745
742 746 imgl = Magick::ImageList.new
743 747 imgl.new_image(subject_width+g_width+1, height)
744 748 gc = Magick::Draw.new
745 749
746 750 # Subjects
747 751 subjects(:image => gc, :top => (headers_heigth + 20), :indent => 4, :format => :image)
748 752
749 753 # Months headers
750 754 month_f = @date_from
751 755 left = subject_width
752 756 @months.times do
753 757 width = ((month_f >> 1) - month_f) * zoom
754 758 gc.fill('white')
755 759 gc.stroke('grey')
756 760 gc.stroke_width(1)
757 761 gc.rectangle(left, 0, left + width, height)
758 762 gc.fill('black')
759 763 gc.stroke('transparent')
760 764 gc.stroke_width(1)
761 765 gc.text(left.round + 8, 14, "#{month_f.year}-#{month_f.month}")
762 766 left = left + width
763 767 month_f = month_f >> 1
764 768 end
765 769
766 770 # Weeks headers
767 771 if show_weeks
768 772 left = subject_width
769 773 height = header_heigth
770 774 if @date_from.cwday == 1
771 775 # date_from is monday
772 776 week_f = date_from
773 777 else
774 778 # find next monday after date_from
775 779 week_f = @date_from + (7 - @date_from.cwday + 1)
776 780 width = (7 - @date_from.cwday + 1) * zoom
777 781 gc.fill('white')
778 782 gc.stroke('grey')
779 783 gc.stroke_width(1)
780 784 gc.rectangle(left, header_heigth, left + width, 2*header_heigth + g_height-1)
781 785 left = left + width
782 786 end
783 787 while week_f <= date_to
784 788 width = (week_f + 6 <= date_to) ? 7 * zoom : (date_to - week_f + 1) * zoom
785 789 gc.fill('white')
786 790 gc.stroke('grey')
787 791 gc.stroke_width(1)
788 792 gc.rectangle(left.round, header_heigth, left.round + width, 2*header_heigth + g_height-1)
789 793 gc.fill('black')
790 794 gc.stroke('transparent')
791 795 gc.stroke_width(1)
792 796 gc.text(left.round + 2, header_heigth + 14, week_f.cweek.to_s)
793 797 left = left + width
794 798 week_f = week_f+7
795 799 end
796 800 end
797 801
798 802 # Days details (week-end in grey)
799 803 if show_days
800 804 left = subject_width
801 805 height = g_height + header_heigth - 1
802 806 wday = @date_from.cwday
803 807 (date_to - @date_from + 1).to_i.times do
804 808 width = zoom
805 809 gc.fill(wday == 6 || wday == 7 ? '#eee' : 'white')
806 810 gc.stroke('grey')
807 811 gc.stroke_width(1)
808 812 gc.rectangle(left, 2*header_heigth, left + width, 2*header_heigth + g_height-1)
809 813 left = left + width
810 814 wday = wday + 1
811 815 wday = 1 if wday > 7
812 816 end
813 817 end
814 818
815 819 # border
816 820 gc.fill('transparent')
817 821 gc.stroke('grey')
818 822 gc.stroke_width(1)
819 823 gc.rectangle(0, 0, subject_width+g_width, headers_heigth)
820 824 gc.stroke('black')
821 825 gc.rectangle(0, 0, subject_width+g_width, g_height+ headers_heigth-1)
822 826
823 827 # content
824 828 top = headers_heigth + 20
825 829
826 830 lines(:image => gc, :top => top, :zoom => zoom, :subject_width => subject_width, :format => :image)
827 831
828 832 # today red line
829 833 if Date.today >= @date_from and Date.today <= date_to
830 834 gc.stroke('red')
831 835 x = (Date.today-@date_from+1)*zoom + subject_width
832 836 gc.line(x, headers_heigth, x, headers_heigth + g_height-1)
833 837 end
834 838
835 839 gc.draw(imgl)
836 840 imgl.format = format
837 841 imgl.to_blob
838 842 end if Object.const_defined?(:Magick)
839 843
840 844 def to_pdf
841 845 pdf = ::Redmine::Export::PDF::IFPDF.new(current_language)
842 846 pdf.SetTitle("#{l(:label_gantt)} #{project}")
843 847 pdf.AliasNbPages
844 848 pdf.footer_date = format_date(Date.today)
845 849 pdf.AddPage("L")
846 850 pdf.SetFontStyle('B',12)
847 851 pdf.SetX(15)
848 852 pdf.Cell(PDF::LeftPaneWidth, 20, project.to_s)
849 853 pdf.Ln
850 854 pdf.SetFontStyle('B',9)
851 855
852 856 subject_width = PDF::LeftPaneWidth
853 857 header_heigth = 5
854 858
855 859 headers_heigth = header_heigth
856 860 show_weeks = false
857 861 show_days = false
858 862
859 863 if self.months < 7
860 864 show_weeks = true
861 865 headers_heigth = 2*header_heigth
862 866 if self.months < 3
863 867 show_days = true
864 868 headers_heigth = 3*header_heigth
865 869 end
866 870 end
867 871
868 872 g_width = PDF.right_pane_width
869 873 zoom = (g_width) / (self.date_to - self.date_from + 1)
870 874 g_height = 120
871 875 t_height = g_height + headers_heigth
872 876
873 877 y_start = pdf.GetY
874 878
875 879 # Months headers
876 880 month_f = self.date_from
877 881 left = subject_width
878 882 height = header_heigth
879 883 self.months.times do
880 884 width = ((month_f >> 1) - month_f) * zoom
881 885 pdf.SetY(y_start)
882 886 pdf.SetX(left)
883 887 pdf.Cell(width, height, "#{month_f.year}-#{month_f.month}", "LTR", 0, "C")
884 888 left = left + width
885 889 month_f = month_f >> 1
886 890 end
887 891
888 892 # Weeks headers
889 893 if show_weeks
890 894 left = subject_width
891 895 height = header_heigth
892 896 if self.date_from.cwday == 1
893 897 # self.date_from is monday
894 898 week_f = self.date_from
895 899 else
896 900 # find next monday after self.date_from
897 901 week_f = self.date_from + (7 - self.date_from.cwday + 1)
898 902 width = (7 - self.date_from.cwday + 1) * zoom-1
899 903 pdf.SetY(y_start + header_heigth)
900 904 pdf.SetX(left)
901 905 pdf.Cell(width + 1, height, "", "LTR")
902 906 left = left + width+1
903 907 end
904 908 while week_f <= self.date_to
905 909 width = (week_f + 6 <= self.date_to) ? 7 * zoom : (self.date_to - week_f + 1) * zoom
906 910 pdf.SetY(y_start + header_heigth)
907 911 pdf.SetX(left)
908 912 pdf.Cell(width, height, (width >= 5 ? week_f.cweek.to_s : ""), "LTR", 0, "C")
909 913 left = left + width
910 914 week_f = week_f+7
911 915 end
912 916 end
913 917
914 918 # Days headers
915 919 if show_days
916 920 left = subject_width
917 921 height = header_heigth
918 922 wday = self.date_from.cwday
919 923 pdf.SetFontStyle('B',7)
920 924 (self.date_to - self.date_from + 1).to_i.times do
921 925 width = zoom
922 926 pdf.SetY(y_start + 2 * header_heigth)
923 927 pdf.SetX(left)
924 928 pdf.Cell(width, height, day_name(wday).first, "LTR", 0, "C")
925 929 left = left + width
926 930 wday = wday + 1
927 931 wday = 1 if wday > 7
928 932 end
929 933 end
930 934
931 935 pdf.SetY(y_start)
932 936 pdf.SetX(15)
933 937 pdf.Cell(subject_width+g_width-15, headers_heigth, "", 1)
934 938
935 939 # Tasks
936 940 top = headers_heigth + y_start
937 941 pdf_subjects_and_lines(pdf, {
938 942 :top => top,
939 943 :zoom => zoom,
940 944 :subject_width => subject_width,
941 945 :g_width => g_width
942 946 })
943 947
944 948
945 949 pdf.Line(15, top, subject_width+g_width, top)
946 950 pdf.Output
947 951
948 952
949 953 end
950 954
951 955 private
952 956
953 957 # Renders both the subjects and lines of the Gantt chart for the
954 958 # PDF format
955 959 def pdf_subjects_and_lines(pdf, options = {})
956 960 subject_options = {:indent => 0, :indent_increment => 5, :top_increment => 3, :render => :subject, :format => :pdf, :pdf => pdf}.merge(options)
957 961 line_options = {:indent => 0, :indent_increment => 5, :top_increment => 3, :render => :line, :format => :pdf, :pdf => pdf}.merge(options)
958 962
959 963 if @project
960 964 render_project(@project, subject_options)
961 965 render_project(@project, line_options)
962 966 else
963 967 Project.roots.each do |project|
964 968 render_project(project, subject_options)
965 969 render_project(project, line_options)
966 970 end
967 971 end
968 972 end
969 973
970 974 end
971 975 end
972 976 end
General Comments 0
You need to be logged in to leave comments. Login now