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