@@ -0,0 +1,24 | |||||
|
1 | # redMine - project management software | |||
|
2 | # Copyright (C) 2006 Jean-Philippe Lang | |||
|
3 | # | |||
|
4 | # This program is free software; you can redistribute it and/or | |||
|
5 | # modify it under the terms of the GNU General Public License | |||
|
6 | # as published by the Free Software Foundation; either version 2 | |||
|
7 | # of the License, or (at your option) any later version. | |||
|
8 | # | |||
|
9 | # This program is distributed in the hope that it will be useful, | |||
|
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
|
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
|
12 | # GNU General Public License for more details. | |||
|
13 | # | |||
|
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 | |||
|
16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
|
17 | ||||
|
18 | module GanttHelper | |||
|
19 | def number_of_issues_on_versions(gantt) | |||
|
20 | versions = gantt.events.collect {|event| (event.is_a? Version) ? event : nil}.compact | |||
|
21 | ||||
|
22 | versions.sum {|v| v.fixed_issues.for_gantt.with_query(@query).count} | |||
|
23 | end | |||
|
24 | end |
1 | NO CONTENT: new file 100644, binary diff hidden |
|
NO CONTENT: new file 100644, binary diff hidden |
1 | NO CONTENT: new file 100644, binary diff hidden |
|
NO CONTENT: new file 100644, binary diff hidden |
1 | NO CONTENT: new file 100644, binary diff hidden |
|
NO CONTENT: new file 100644, binary diff hidden |
1 | NO CONTENT: new file 100644, binary diff hidden |
|
NO CONTENT: new file 100644, binary diff hidden |
1 | NO CONTENT: new file 100644, binary diff hidden |
|
NO CONTENT: new file 100644, binary diff hidden |
This diff has been collapsed as it changes many lines, (703 lines changed) Show them Hide them | |||||
@@ -0,0 +1,703 | |||||
|
1 | # redMine - project management software | |||
|
2 | # Copyright (C) 2006-2008 Jean-Philippe Lang | |||
|
3 | # | |||
|
4 | # This program is free software; you can redistribute it and/or | |||
|
5 | # modify it under the terms of the GNU General Public License | |||
|
6 | # as published by the Free Software Foundation; either version 2 | |||
|
7 | # of the License, or (at your option) any later version. | |||
|
8 | # | |||
|
9 | # This program is distributed in the hope that it will be useful, | |||
|
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
|
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
|
12 | # GNU General Public License for more details. | |||
|
13 | # | |||
|
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 | |||
|
16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
|
17 | ||||
|
18 | require File.dirname(__FILE__) + '/../../../../test_helper' | |||
|
19 | ||||
|
20 | class Redmine::Helpers::GanttTest < ActiveSupport::TestCase | |||
|
21 | # Utility methods and classes so assert_select can be used. | |||
|
22 | class GanttViewTest < ActionView::Base | |||
|
23 | include ActionView::Helpers::UrlHelper | |||
|
24 | include ActionView::Helpers::TextHelper | |||
|
25 | include ActionController::UrlWriter | |||
|
26 | include ApplicationHelper | |||
|
27 | include ProjectsHelper | |||
|
28 | include IssuesHelper | |||
|
29 | ||||
|
30 | def self.default_url_options | |||
|
31 | {:only_path => true } | |||
|
32 | end | |||
|
33 | ||||
|
34 | end | |||
|
35 | ||||
|
36 | include ActionController::Assertions::SelectorAssertions | |||
|
37 | ||||
|
38 | def setup | |||
|
39 | @response = ActionController::TestResponse.new | |||
|
40 | # Fixtures | |||
|
41 | ProjectCustomField.delete_all | |||
|
42 | Project.destroy_all | |||
|
43 | ||||
|
44 | User.current = User.find(1) | |||
|
45 | end | |||
|
46 | ||||
|
47 | def build_view | |||
|
48 | @view = GanttViewTest.new | |||
|
49 | end | |||
|
50 | ||||
|
51 | def html_document | |||
|
52 | HTML::Document.new(@response.body) | |||
|
53 | end | |||
|
54 | ||||
|
55 | # Creates a Gantt chart for a 4 week span | |||
|
56 | def create_gantt(project=Project.generate!) | |||
|
57 | @project = project | |||
|
58 | @gantt = Redmine::Helpers::Gantt.new | |||
|
59 | @gantt.project = @project | |||
|
60 | @gantt.query = Query.generate_default!(:project => @project) | |||
|
61 | @gantt.view = build_view | |||
|
62 | @gantt.instance_variable_set('@date_from', 2.weeks.ago.to_date) | |||
|
63 | @gantt.instance_variable_set('@date_to', 2.weeks.from_now.to_date) | |||
|
64 | end | |||
|
65 | ||||
|
66 | context "#number_of_rows" do | |||
|
67 | ||||
|
68 | context "with one project" do | |||
|
69 | should "return the number of rows just for that project" | |||
|
70 | end | |||
|
71 | ||||
|
72 | context "with no project" do | |||
|
73 | should "return the total number of rows for all the projects, resursively" | |||
|
74 | end | |||
|
75 | ||||
|
76 | end | |||
|
77 | ||||
|
78 | context "#number_of_rows_on_project" do | |||
|
79 | setup do | |||
|
80 | create_gantt | |||
|
81 | end | |||
|
82 | ||||
|
83 | should "clear the @query.project so cross-project issues and versions can be counted" do | |||
|
84 | assert @gantt.query.project | |||
|
85 | @gantt.number_of_rows_on_project(@project) | |||
|
86 | assert_nil @gantt.query.project | |||
|
87 | end | |||
|
88 | ||||
|
89 | should "count 1 for the project itself" do | |||
|
90 | assert_equal 1, @gantt.number_of_rows_on_project(@project) | |||
|
91 | end | |||
|
92 | ||||
|
93 | should "count the number of issues without a version" do | |||
|
94 | @project.issues << Issue.generate_for_project!(@project, :fixed_version => nil) | |||
|
95 | assert_equal 2, @gantt.number_of_rows_on_project(@project) | |||
|
96 | end | |||
|
97 | ||||
|
98 | should "count the number of versions" do | |||
|
99 | @project.versions << Version.generate! | |||
|
100 | @project.versions << Version.generate! | |||
|
101 | assert_equal 3, @gantt.number_of_rows_on_project(@project) | |||
|
102 | end | |||
|
103 | ||||
|
104 | should "count the number of issues on versions, including cross-project" do | |||
|
105 | version = Version.generate! | |||
|
106 | @project.versions << version | |||
|
107 | @project.issues << Issue.generate_for_project!(@project, :fixed_version => version) | |||
|
108 | ||||
|
109 | assert_equal 3, @gantt.number_of_rows_on_project(@project) | |||
|
110 | end | |||
|
111 | ||||
|
112 | should "recursive and count the number of rows on each subproject" do | |||
|
113 | @project.versions << Version.generate! # +1 | |||
|
114 | ||||
|
115 | @subproject = Project.generate!(:enabled_module_names => ['issue_tracking']) # +1 | |||
|
116 | @subproject.set_parent!(@project) | |||
|
117 | @subproject.issues << Issue.generate_for_project!(@subproject) # +1 | |||
|
118 | @subproject.issues << Issue.generate_for_project!(@subproject) # +1 | |||
|
119 | ||||
|
120 | @subsubproject = Project.generate!(:enabled_module_names => ['issue_tracking']) # +1 | |||
|
121 | @subsubproject.set_parent!(@subproject) | |||
|
122 | @subsubproject.issues << Issue.generate_for_project!(@subsubproject) # +1 | |||
|
123 | ||||
|
124 | assert_equal 7, @gantt.number_of_rows_on_project(@project) # +1 for self | |||
|
125 | end | |||
|
126 | end | |||
|
127 | ||||
|
128 | # TODO: more of an integration test | |||
|
129 | context "#subjects" do | |||
|
130 | setup do | |||
|
131 | create_gantt | |||
|
132 | @project.enabled_module_names = [:issue_tracking] | |||
|
133 | @tracker = Tracker.generate! | |||
|
134 | @project.trackers << @tracker | |||
|
135 | @version = Version.generate!(:effective_date => 1.week.from_now.to_date, :sharing => 'none') | |||
|
136 | @project.versions << @version | |||
|
137 | ||||
|
138 | @issue = Issue.generate!(:fixed_version => @version, | |||
|
139 | :subject => "gantt#line_for_project", | |||
|
140 | :tracker => @tracker, | |||
|
141 | :project => @project, | |||
|
142 | :done_ratio => 30, | |||
|
143 | :start_date => Date.yesterday, | |||
|
144 | :due_date => 1.week.from_now.to_date) | |||
|
145 | @project.issues << @issue | |||
|
146 | ||||
|
147 | @response.body = @gantt.subjects | |||
|
148 | end | |||
|
149 | ||||
|
150 | context "project" do | |||
|
151 | should "be rendered" do | |||
|
152 | assert_select "div.project-name a", /#{@project.name}/ | |||
|
153 | end | |||
|
154 | ||||
|
155 | should "have an indent of 4" do | |||
|
156 | assert_select "div.project-name[style*=left:4px]" | |||
|
157 | end | |||
|
158 | end | |||
|
159 | ||||
|
160 | context "version" do | |||
|
161 | should "be rendered" do | |||
|
162 | assert_select "div.version-name a", /#{@version.name}/ | |||
|
163 | end | |||
|
164 | ||||
|
165 | should "be indented 24 (one level)" do | |||
|
166 | assert_select "div.version-name[style*=left:24px]" | |||
|
167 | end | |||
|
168 | end | |||
|
169 | ||||
|
170 | context "issue" do | |||
|
171 | should "be rendered" do | |||
|
172 | assert_select "div.issue-subject", /#{@issue.subject}/ | |||
|
173 | end | |||
|
174 | ||||
|
175 | should "be indented 44 (two levels)" do | |||
|
176 | assert_select "div.issue-subject[style*=left:44px]" | |||
|
177 | end | |||
|
178 | end | |||
|
179 | end | |||
|
180 | ||||
|
181 | context "#lines" do | |||
|
182 | setup do | |||
|
183 | create_gantt | |||
|
184 | @project.enabled_module_names = [:issue_tracking] | |||
|
185 | @tracker = Tracker.generate! | |||
|
186 | @project.trackers << @tracker | |||
|
187 | @version = Version.generate!(:effective_date => 1.week.from_now.to_date) | |||
|
188 | @project.versions << @version | |||
|
189 | @issue = Issue.generate!(:fixed_version => @version, | |||
|
190 | :subject => "gantt#line_for_project", | |||
|
191 | :tracker => @tracker, | |||
|
192 | :project => @project, | |||
|
193 | :done_ratio => 30, | |||
|
194 | :start_date => Date.yesterday, | |||
|
195 | :due_date => 1.week.from_now.to_date) | |||
|
196 | @project.issues << @issue | |||
|
197 | ||||
|
198 | @response.body = @gantt.lines | |||
|
199 | end | |||
|
200 | ||||
|
201 | context "project" do | |||
|
202 | should "be rendered" do | |||
|
203 | assert_select "div.project_todo" | |||
|
204 | assert_select "div.project-line.starting" | |||
|
205 | assert_select "div.project-line.ending" | |||
|
206 | assert_select "div.label.project-name", /#{@project.name}/ | |||
|
207 | end | |||
|
208 | end | |||
|
209 | ||||
|
210 | context "version" do | |||
|
211 | should "be rendered" do | |||
|
212 | assert_select "div.milestone_todo" | |||
|
213 | assert_select "div.milestone.starting" | |||
|
214 | assert_select "div.milestone.ending" | |||
|
215 | assert_select "div.label.version-name", /#{@version.name}/ | |||
|
216 | end | |||
|
217 | end | |||
|
218 | ||||
|
219 | context "issue" do | |||
|
220 | should "be rendered" do | |||
|
221 | assert_select "div.task_todo" | |||
|
222 | assert_select "div.label.issue-name", /#{@issue.done_ratio}/ | |||
|
223 | assert_select "div.tooltip", /#{@issue.subject}/ | |||
|
224 | end | |||
|
225 | end | |||
|
226 | end | |||
|
227 | ||||
|
228 | context "#render_project" do | |||
|
229 | should "be tested" | |||
|
230 | end | |||
|
231 | ||||
|
232 | context "#render_issues" do | |||
|
233 | should "be tested" | |||
|
234 | end | |||
|
235 | ||||
|
236 | context "#render_version" do | |||
|
237 | should "be tested" | |||
|
238 | end | |||
|
239 | ||||
|
240 | context "#subject_for_project" do | |||
|
241 | setup do | |||
|
242 | create_gantt | |||
|
243 | end | |||
|
244 | ||||
|
245 | context ":html format" do | |||
|
246 | should "add an absolute positioned div" do | |||
|
247 | @response.body = @gantt.subject_for_project(@project, {:format => :html}) | |||
|
248 | assert_select "div[style*=absolute]" | |||
|
249 | end | |||
|
250 | ||||
|
251 | should "use the indent option to move the div to the right" do | |||
|
252 | @response.body = @gantt.subject_for_project(@project, {:format => :html, :indent => 40}) | |||
|
253 | assert_select "div[style*=left:40]" | |||
|
254 | end | |||
|
255 | ||||
|
256 | should "include the project name" do | |||
|
257 | @response.body = @gantt.subject_for_project(@project, {:format => :html}) | |||
|
258 | assert_select 'div', :text => /#{@project.name}/ | |||
|
259 | end | |||
|
260 | ||||
|
261 | should "include a link to the project" do | |||
|
262 | @response.body = @gantt.subject_for_project(@project, {:format => :html}) | |||
|
263 | assert_select 'a[href=?]', "/projects/#{@project.identifier}", :text => /#{@project.name}/ | |||
|
264 | end | |||
|
265 | ||||
|
266 | should "style overdue projects" do | |||
|
267 | @project.enabled_module_names = [:issue_tracking] | |||
|
268 | @project.versions << Version.generate!(:effective_date => Date.yesterday) | |||
|
269 | ||||
|
270 | assert @project.overdue?, "Need an overdue project for this test" | |||
|
271 | @response.body = @gantt.subject_for_project(@project, {:format => :html}) | |||
|
272 | ||||
|
273 | assert_select 'div span.project-overdue' | |||
|
274 | end | |||
|
275 | ||||
|
276 | ||||
|
277 | end | |||
|
278 | ||||
|
279 | should "test the PNG format" | |||
|
280 | should "test the PDF format" | |||
|
281 | end | |||
|
282 | ||||
|
283 | context "#line_for_project" do | |||
|
284 | setup do | |||
|
285 | create_gantt | |||
|
286 | @project.enabled_module_names = [:issue_tracking] | |||
|
287 | @tracker = Tracker.generate! | |||
|
288 | @project.trackers << @tracker | |||
|
289 | @version = Version.generate!(:effective_date => Date.yesterday) | |||
|
290 | @project.versions << @version | |||
|
291 | ||||
|
292 | @project.issues << Issue.generate!(:fixed_version => @version, | |||
|
293 | :subject => "gantt#line_for_project", | |||
|
294 | :tracker => @tracker, | |||
|
295 | :project => @project, | |||
|
296 | :done_ratio => 30, | |||
|
297 | :start_date => Date.yesterday, | |||
|
298 | :due_date => 1.week.from_now.to_date) | |||
|
299 | end | |||
|
300 | ||||
|
301 | context ":html format" do | |||
|
302 | context "todo line" do | |||
|
303 | should "start from the starting point on the left" do | |||
|
304 | @response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4}) | |||
|
305 | assert_select "div.project_todo[style*=left:52px]" | |||
|
306 | end | |||
|
307 | ||||
|
308 | should "be the total width of the project" do | |||
|
309 | @response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4}) | |||
|
310 | assert_select "div.project_todo[style*=width:31px]" | |||
|
311 | end | |||
|
312 | ||||
|
313 | end | |||
|
314 | ||||
|
315 | context "late line" do | |||
|
316 | should "start from the starting point on the left" do | |||
|
317 | @response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4}) | |||
|
318 | assert_select "div.project_late[style*=left:52px]" | |||
|
319 | end | |||
|
320 | ||||
|
321 | should "be the total delayed width of the project" do | |||
|
322 | @response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4}) | |||
|
323 | assert_select "div.project_late[style*=width:6px]" | |||
|
324 | end | |||
|
325 | end | |||
|
326 | ||||
|
327 | context "done line" do | |||
|
328 | should "start from the starting point on the left" do | |||
|
329 | @response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4}) | |||
|
330 | assert_select "div.project_done[style*=left:52px]" | |||
|
331 | end | |||
|
332 | ||||
|
333 | should "Be the total done width of the project" do | |||
|
334 | @response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4}) | |||
|
335 | assert_select "div.project_done[style*=left:52px]" | |||
|
336 | end | |||
|
337 | end | |||
|
338 | ||||
|
339 | context "starting marker" do | |||
|
340 | should "not appear if the starting point is off the gantt chart" do | |||
|
341 | # Shift the date range of the chart | |||
|
342 | @gantt.instance_variable_set('@date_from', Date.today) | |||
|
343 | ||||
|
344 | @response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4}) | |||
|
345 | assert_select "div.project-line.starting", false | |||
|
346 | end | |||
|
347 | ||||
|
348 | should "appear at the starting point" do | |||
|
349 | @response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4}) | |||
|
350 | assert_select "div.project-line.starting[style*=left:52px]" | |||
|
351 | end | |||
|
352 | end | |||
|
353 | ||||
|
354 | context "ending marker" do | |||
|
355 | should "not appear if the starting point is off the gantt chart" do | |||
|
356 | # Shift the date range of the chart | |||
|
357 | @gantt.instance_variable_set('@date_to', 2.weeks.ago.to_date) | |||
|
358 | ||||
|
359 | @response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4}) | |||
|
360 | assert_select "div.project-line.ending", false | |||
|
361 | ||||
|
362 | end | |||
|
363 | ||||
|
364 | should "appear at the end of the date range" do | |||
|
365 | @response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4}) | |||
|
366 | assert_select "div.project-line.ending[style*=left:84px]" | |||
|
367 | end | |||
|
368 | end | |||
|
369 | ||||
|
370 | context "status content" do | |||
|
371 | should "appear at the far left, even if it's far in the past" do | |||
|
372 | @gantt.instance_variable_set('@date_to', 2.weeks.ago.to_date) | |||
|
373 | ||||
|
374 | @response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4}) | |||
|
375 | assert_select "div.project-name", /#{@project.name}/ | |||
|
376 | end | |||
|
377 | ||||
|
378 | should "show the project name" do | |||
|
379 | @response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4}) | |||
|
380 | assert_select "div.project-name", /#{@project.name}/ | |||
|
381 | end | |||
|
382 | ||||
|
383 | should "show the percent complete" do | |||
|
384 | @response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4}) | |||
|
385 | assert_select "div.project-name", /0%/ | |||
|
386 | end | |||
|
387 | end | |||
|
388 | end | |||
|
389 | ||||
|
390 | should "test the PNG format" | |||
|
391 | should "test the PDF format" | |||
|
392 | end | |||
|
393 | ||||
|
394 | context "#subject_for_version" do | |||
|
395 | setup do | |||
|
396 | create_gantt | |||
|
397 | @project.enabled_module_names = [:issue_tracking] | |||
|
398 | @tracker = Tracker.generate! | |||
|
399 | @project.trackers << @tracker | |||
|
400 | @version = Version.generate!(:effective_date => Date.yesterday) | |||
|
401 | @project.versions << @version | |||
|
402 | ||||
|
403 | @project.issues << Issue.generate!(:fixed_version => @version, | |||
|
404 | :subject => "gantt#subject_for_version", | |||
|
405 | :tracker => @tracker, | |||
|
406 | :project => @project, | |||
|
407 | :start_date => Date.today) | |||
|
408 | ||||
|
409 | end | |||
|
410 | ||||
|
411 | context ":html format" do | |||
|
412 | should "add an absolute positioned div" do | |||
|
413 | @response.body = @gantt.subject_for_version(@version, {:format => :html}) | |||
|
414 | assert_select "div[style*=absolute]" | |||
|
415 | end | |||
|
416 | ||||
|
417 | should "use the indent option to move the div to the right" do | |||
|
418 | @response.body = @gantt.subject_for_version(@version, {:format => :html, :indent => 40}) | |||
|
419 | assert_select "div[style*=left:40]" | |||
|
420 | end | |||
|
421 | ||||
|
422 | should "include the version name" do | |||
|
423 | @response.body = @gantt.subject_for_version(@version, {:format => :html}) | |||
|
424 | assert_select 'div', :text => /#{@version.name}/ | |||
|
425 | end | |||
|
426 | ||||
|
427 | should "include a link to the version" do | |||
|
428 | @response.body = @gantt.subject_for_version(@version, {:format => :html}) | |||
|
429 | assert_select 'a[href=?]', Regexp.escape("/versions/show/#{@version.to_param}"), :text => /#{@version.name}/ | |||
|
430 | end | |||
|
431 | ||||
|
432 | should "style late versions" do | |||
|
433 | assert @version.overdue?, "Need an overdue version for this test" | |||
|
434 | @response.body = @gantt.subject_for_version(@version, {:format => :html}) | |||
|
435 | ||||
|
436 | assert_select 'div span.version-behind-schedule' | |||
|
437 | end | |||
|
438 | ||||
|
439 | should "style behind schedule versions" do | |||
|
440 | assert @version.behind_schedule?, "Need a behind schedule version for this test" | |||
|
441 | @response.body = @gantt.subject_for_version(@version, {:format => :html}) | |||
|
442 | ||||
|
443 | assert_select 'div span.version-behind-schedule' | |||
|
444 | end | |||
|
445 | end | |||
|
446 | should "test the PNG format" | |||
|
447 | should "test the PDF format" | |||
|
448 | end | |||
|
449 | ||||
|
450 | context "#line_for_version" do | |||
|
451 | setup do | |||
|
452 | create_gantt | |||
|
453 | @project.enabled_module_names = [:issue_tracking] | |||
|
454 | @tracker = Tracker.generate! | |||
|
455 | @project.trackers << @tracker | |||
|
456 | @version = Version.generate!(:effective_date => 1.week.from_now.to_date) | |||
|
457 | @project.versions << @version | |||
|
458 | ||||
|
459 | @project.issues << Issue.generate!(:fixed_version => @version, | |||
|
460 | :subject => "gantt#line_for_project", | |||
|
461 | :tracker => @tracker, | |||
|
462 | :project => @project, | |||
|
463 | :done_ratio => 30, | |||
|
464 | :start_date => Date.yesterday, | |||
|
465 | :due_date => 1.week.from_now.to_date) | |||
|
466 | end | |||
|
467 | ||||
|
468 | context ":html format" do | |||
|
469 | context "todo line" do | |||
|
470 | should "start from the starting point on the left" do | |||
|
471 | @response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4}) | |||
|
472 | assert_select "div.milestone_todo[style*=left:52px]" | |||
|
473 | end | |||
|
474 | ||||
|
475 | should "be the total width of the version" do | |||
|
476 | @response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4}) | |||
|
477 | assert_select "div.milestone_todo[style*=width:31px]" | |||
|
478 | end | |||
|
479 | ||||
|
480 | end | |||
|
481 | ||||
|
482 | context "late line" do | |||
|
483 | should "start from the starting point on the left" do | |||
|
484 | @response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4}) | |||
|
485 | assert_select "div.milestone_late[style*=left:52px]" | |||
|
486 | end | |||
|
487 | ||||
|
488 | should "be the total delayed width of the version" do | |||
|
489 | @response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4}) | |||
|
490 | assert_select "div.milestone_late[style*=width:6px]" | |||
|
491 | end | |||
|
492 | end | |||
|
493 | ||||
|
494 | context "done line" do | |||
|
495 | should "start from the starting point on the left" do | |||
|
496 | @response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4}) | |||
|
497 | assert_select "div.milestone_done[style*=left:52px]" | |||
|
498 | end | |||
|
499 | ||||
|
500 | should "Be the total done width of the version" do | |||
|
501 | @response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4}) | |||
|
502 | assert_select "div.milestone_done[style*=left:52px]" | |||
|
503 | end | |||
|
504 | end | |||
|
505 | ||||
|
506 | context "starting marker" do | |||
|
507 | should "not appear if the starting point is off the gantt chart" do | |||
|
508 | # Shift the date range of the chart | |||
|
509 | @gantt.instance_variable_set('@date_from', Date.today) | |||
|
510 | ||||
|
511 | @response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4}) | |||
|
512 | assert_select "div.milestone.starting", false | |||
|
513 | end | |||
|
514 | ||||
|
515 | should "appear at the starting point" do | |||
|
516 | @response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4}) | |||
|
517 | assert_select "div.milestone.starting[style*=left:52px]" | |||
|
518 | end | |||
|
519 | end | |||
|
520 | ||||
|
521 | context "ending marker" do | |||
|
522 | should "not appear if the starting point is off the gantt chart" do | |||
|
523 | # Shift the date range of the chart | |||
|
524 | @gantt.instance_variable_set('@date_to', 2.weeks.ago.to_date) | |||
|
525 | ||||
|
526 | @response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4}) | |||
|
527 | assert_select "div.milestone.ending", false | |||
|
528 | ||||
|
529 | end | |||
|
530 | ||||
|
531 | should "appear at the end of the date range" do | |||
|
532 | @response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4}) | |||
|
533 | assert_select "div.milestone.ending[style*=left:84px]" | |||
|
534 | end | |||
|
535 | end | |||
|
536 | ||||
|
537 | context "status content" do | |||
|
538 | should "appear at the far left, even if it's far in the past" do | |||
|
539 | @gantt.instance_variable_set('@date_to', 2.weeks.ago.to_date) | |||
|
540 | ||||
|
541 | @response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4}) | |||
|
542 | assert_select "div.version-name", /#{@version.name}/ | |||
|
543 | end | |||
|
544 | ||||
|
545 | should "show the version name" do | |||
|
546 | @response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4}) | |||
|
547 | assert_select "div.version-name", /#{@version.name}/ | |||
|
548 | end | |||
|
549 | ||||
|
550 | should "show the percent complete" do | |||
|
551 | @response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4}) | |||
|
552 | assert_select "div.version-name", /30%/ | |||
|
553 | end | |||
|
554 | end | |||
|
555 | end | |||
|
556 | ||||
|
557 | should "test the PNG format" | |||
|
558 | should "test the PDF format" | |||
|
559 | end | |||
|
560 | ||||
|
561 | context "#subject_for_issue" do | |||
|
562 | setup do | |||
|
563 | create_gantt | |||
|
564 | @project.enabled_module_names = [:issue_tracking] | |||
|
565 | @tracker = Tracker.generate! | |||
|
566 | @project.trackers << @tracker | |||
|
567 | ||||
|
568 | @issue = Issue.generate!(:subject => "gantt#subject_for_issue", | |||
|
569 | :tracker => @tracker, | |||
|
570 | :project => @project, | |||
|
571 | :start_date => 3.days.ago.to_date, | |||
|
572 | :due_date => Date.yesterday) | |||
|
573 | @project.issues << @issue | |||
|
574 | ||||
|
575 | end | |||
|
576 | ||||
|
577 | context ":html format" do | |||
|
578 | should "add an absolute positioned div" do | |||
|
579 | @response.body = @gantt.subject_for_issue(@issue, {:format => :html}) | |||
|
580 | assert_select "div[style*=absolute]" | |||
|
581 | end | |||
|
582 | ||||
|
583 | should "use the indent option to move the div to the right" do | |||
|
584 | @response.body = @gantt.subject_for_issue(@issue, {:format => :html, :indent => 40}) | |||
|
585 | assert_select "div[style*=left:40]" | |||
|
586 | end | |||
|
587 | ||||
|
588 | should "include the issue subject" do | |||
|
589 | @response.body = @gantt.subject_for_issue(@issue, {:format => :html}) | |||
|
590 | assert_select 'div', :text => /#{@issue.subject}/ | |||
|
591 | end | |||
|
592 | ||||
|
593 | should "include a link to the issue" do | |||
|
594 | @response.body = @gantt.subject_for_issue(@issue, {:format => :html}) | |||
|
595 | assert_select 'a[href=?]', Regexp.escape("/issues/#{@issue.to_param}"), :text => /#{@tracker.name} ##{@issue.id}/ | |||
|
596 | end | |||
|
597 | ||||
|
598 | should "style overdue issues" do | |||
|
599 | assert @issue.overdue?, "Need an overdue issue for this test" | |||
|
600 | @response.body = @gantt.subject_for_issue(@issue, {:format => :html}) | |||
|
601 | ||||
|
602 | assert_select 'div span.issue-overdue' | |||
|
603 | end | |||
|
604 | ||||
|
605 | end | |||
|
606 | should "test the PNG format" | |||
|
607 | should "test the PDF format" | |||
|
608 | end | |||
|
609 | ||||
|
610 | context "#line_for_issue" do | |||
|
611 | setup do | |||
|
612 | create_gantt | |||
|
613 | @project.enabled_module_names = [:issue_tracking] | |||
|
614 | @tracker = Tracker.generate! | |||
|
615 | @project.trackers << @tracker | |||
|
616 | @version = Version.generate!(:effective_date => 1.week.from_now.to_date) | |||
|
617 | @project.versions << @version | |||
|
618 | @issue = Issue.generate!(:fixed_version => @version, | |||
|
619 | :subject => "gantt#line_for_project", | |||
|
620 | :tracker => @tracker, | |||
|
621 | :project => @project, | |||
|
622 | :done_ratio => 30, | |||
|
623 | :start_date => Date.yesterday, | |||
|
624 | :due_date => 1.week.from_now.to_date) | |||
|
625 | @project.issues << @issue | |||
|
626 | end | |||
|
627 | ||||
|
628 | context ":html format" do | |||
|
629 | context "todo line" do | |||
|
630 | should "start from the starting point on the left" do | |||
|
631 | @response.body = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4}) | |||
|
632 | assert_select "div.task_todo[style*=left:52px]" | |||
|
633 | end | |||
|
634 | ||||
|
635 | should "be the total width of the issue" do | |||
|
636 | @response.body = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4}) | |||
|
637 | assert_select "div.task_todo[style*=width:34px]" | |||
|
638 | end | |||
|
639 | ||||
|
640 | end | |||
|
641 | ||||
|
642 | context "late line" do | |||
|
643 | should "start from the starting point on the left" do | |||
|
644 | @response.body = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4}) | |||
|
645 | assert_select "div.task_late[style*=left:52px]" | |||
|
646 | end | |||
|
647 | ||||
|
648 | should "be the total delayed width of the issue" do | |||
|
649 | @response.body = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4}) | |||
|
650 | assert_select "div.task_late[style*=width:6px]" | |||
|
651 | end | |||
|
652 | end | |||
|
653 | ||||
|
654 | context "done line" do | |||
|
655 | should "start from the starting point on the left" do | |||
|
656 | @response.body = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4}) | |||
|
657 | assert_select "div.task_done[style*=left:52px]" | |||
|
658 | end | |||
|
659 | ||||
|
660 | should "Be the total done width of the issue" do | |||
|
661 | @response.body = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4}) | |||
|
662 | assert_select "div.task_done[style*=left:52px]" | |||
|
663 | end | |||
|
664 | end | |||
|
665 | ||||
|
666 | context "status content" do | |||
|
667 | should "appear at the far left, even if it's far in the past" do | |||
|
668 | @gantt.instance_variable_set('@date_to', 2.weeks.ago.to_date) | |||
|
669 | ||||
|
670 | @response.body = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4}) | |||
|
671 | assert_select "div.issue-name" | |||
|
672 | end | |||
|
673 | ||||
|
674 | should "show the issue status" do | |||
|
675 | @response.body = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4}) | |||
|
676 | assert_select "div.issue-name", /#{@issue.status.name}/ | |||
|
677 | end | |||
|
678 | ||||
|
679 | should "show the percent complete" do | |||
|
680 | @response.body = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4}) | |||
|
681 | assert_select "div.issue-name", /30%/ | |||
|
682 | end | |||
|
683 | end | |||
|
684 | end | |||
|
685 | ||||
|
686 | should "have an issue tooltip" do | |||
|
687 | @response.body = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4}) | |||
|
688 | assert_select "div.tooltip", /#{@issue.subject}/ | |||
|
689 | end | |||
|
690 | ||||
|
691 | should "test the PNG format" | |||
|
692 | should "test the PDF format" | |||
|
693 | end | |||
|
694 | ||||
|
695 | context "#to_image" do | |||
|
696 | should "be tested" | |||
|
697 | end | |||
|
698 | ||||
|
699 | context "#to_pdf" do | |||
|
700 | should "be tested" | |||
|
701 | end | |||
|
702 | ||||
|
703 | end |
@@ -4,6 +4,7 class GanttsController < ApplicationController | |||||
4 |
|
4 | |||
5 | rescue_from Query::StatementInvalid, :with => :query_statement_invalid |
|
5 | rescue_from Query::StatementInvalid, :with => :query_statement_invalid | |
6 |
|
6 | |||
|
7 | helper :gantt | |||
7 | helper :issues |
|
8 | helper :issues | |
8 | helper :projects |
|
9 | helper :projects | |
9 | helper :queries |
|
10 | helper :queries | |
@@ -14,32 +15,17 class GanttsController < ApplicationController | |||||
14 |
|
15 | |||
15 | def show |
|
16 | def show | |
16 | @gantt = Redmine::Helpers::Gantt.new(params) |
|
17 | @gantt = Redmine::Helpers::Gantt.new(params) | |
|
18 | @gantt.project = @project | |||
17 | retrieve_query |
|
19 | retrieve_query | |
18 | @query.group_by = nil |
|
20 | @query.group_by = nil | |
19 | if @query.valid? |
|
21 | @gantt.query = @query if @query.valid? | |
20 | events = [] |
|
|||
21 | # Issues that have start and due dates |
|
|||
22 | events += @query.issues(:include => [:tracker, :assigned_to, :priority], |
|
|||
23 | :order => "start_date, due_date", |
|
|||
24 | :conditions => ["(((start_date>=? and start_date<=?) or (due_date>=? and due_date<=?) or (start_date<? and due_date>?)) and start_date is not null and due_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to] |
|
|||
25 | ) |
|
|||
26 | # Issues that don't have a due date but that are assigned to a version with a date |
|
|||
27 | events += @query.issues(:include => [:tracker, :assigned_to, :priority, :fixed_version], |
|
|||
28 | :order => "start_date, effective_date", |
|
|||
29 | :conditions => ["(((start_date>=? and start_date<=?) or (effective_date>=? and effective_date<=?) or (start_date<? and effective_date>?)) and start_date is not null and due_date is null and effective_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to] |
|
|||
30 | ) |
|
|||
31 | # Versions |
|
|||
32 | events += @query.versions(:conditions => ["effective_date BETWEEN ? AND ?", @gantt.date_from, @gantt.date_to]) |
|
|||
33 |
|
||||
34 | @gantt.events = events |
|
|||
35 | end |
|
|||
36 |
|
22 | |||
37 | basename = (@project ? "#{@project.identifier}-" : '') + 'gantt' |
|
23 | basename = (@project ? "#{@project.identifier}-" : '') + 'gantt' | |
38 |
|
24 | |||
39 | respond_to do |format| |
|
25 | respond_to do |format| | |
40 | format.html { render :action => "show", :layout => !request.xhr? } |
|
26 | format.html { render :action => "show", :layout => !request.xhr? } | |
41 |
format.png { send_data(@gantt.to_image |
|
27 | format.png { send_data(@gantt.to_image, :disposition => 'inline', :type => 'image/png', :filename => "#{basename}.png") } if @gantt.respond_to?('to_image') | |
42 |
format.pdf { send_data( |
|
28 | format.pdf { send_data(@gantt.to_pdf, :type => 'application/pdf', :filename => "#{basename}.pdf") } | |
43 | end |
|
29 | end | |
44 | end |
|
30 | end | |
45 |
|
31 |
@@ -47,6 +47,7 class IssuesController < ApplicationController | |||||
47 | include SortHelper |
|
47 | include SortHelper | |
48 | include IssuesHelper |
|
48 | include IssuesHelper | |
49 | helper :timelog |
|
49 | helper :timelog | |
|
50 | helper :gantt | |||
50 | include Redmine::Export::PDF |
|
51 | include Redmine::Export::PDF | |
51 |
|
52 | |||
52 | verify :method => [:post, :delete], |
|
53 | verify :method => [:post, :delete], |
@@ -121,6 +121,11 module ApplicationHelper | |||||
121 |
|
121 | |||
122 | link_to(text, {:controller => 'repositories', :action => 'revision', :id => project, :rev => revision}, :title => l(:label_revision_id, revision)) |
|
122 | link_to(text, {:controller => 'repositories', :action => 'revision', :id => project, :rev => revision}, :title => l(:label_revision_id, revision)) | |
123 | end |
|
123 | end | |
|
124 | ||||
|
125 | def link_to_project(project, options={}) | |||
|
126 | options[:class] ||= 'project' | |||
|
127 | link_to(h(project), {:controller => 'projects', :action => 'show', :id => project}, :class => options[:class]) | |||
|
128 | end | |||
124 |
|
129 | |||
125 | # Generates a link to a project if active |
|
130 | # Generates a link to a project if active | |
126 | # Examples: |
|
131 | # Examples: | |
@@ -832,6 +837,8 module ApplicationHelper | |||||
832 | email = $1 |
|
837 | email = $1 | |
833 | end |
|
838 | end | |
834 | return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil |
|
839 | return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil | |
|
840 | else | |||
|
841 | '' | |||
835 | end |
|
842 | end | |
836 | end |
|
843 | end | |
837 |
|
844 |
@@ -35,8 +35,10 module IssuesHelper | |||||
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 |
|
38 | @cached_label_project ||= l(:field_project) | ||
|
39 | ||||
39 | 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 />" + | |||
40 | "<strong>#{@cached_label_status}</strong>: #{issue.status.name}<br />" + |
|
42 | "<strong>#{@cached_label_status}</strong>: #{issue.status.name}<br />" + | |
41 | "<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 />" + | |
42 | "<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 />" + |
@@ -62,10 +62,28 class Issue < ActiveRecord::Base | |||||
62 |
|
62 | |||
63 | named_scope :open, :conditions => ["#{IssueStatus.table_name}.is_closed = ?", false], :include => :status |
|
63 | named_scope :open, :conditions => ["#{IssueStatus.table_name}.is_closed = ?", false], :include => :status | |
64 |
|
64 | |||
65 |
named_scope :recently_updated, :order => "#{se |
|
65 | named_scope :recently_updated, :order => "#{Issue.table_name}.updated_on DESC" | |
66 | named_scope :with_limit, lambda { |limit| { :limit => limit} } |
|
66 | named_scope :with_limit, lambda { |limit| { :limit => limit} } | |
67 | named_scope :on_active_project, :include => [:status, :project, :tracker], |
|
67 | named_scope :on_active_project, :include => [:status, :project, :tracker], | |
68 | :conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"] |
|
68 | :conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"] | |
|
69 | named_scope :for_gantt, lambda { | |||
|
70 | { | |||
|
71 | :include => [:tracker, :status, :assigned_to, :priority, :project, :fixed_version], | |||
|
72 | :order => "#{Issue.table_name}.due_date ASC, #{Issue.table_name}.start_date ASC, #{Issue.table_name}.id ASC" | |||
|
73 | } | |||
|
74 | } | |||
|
75 | ||||
|
76 | named_scope :without_version, lambda { | |||
|
77 | { | |||
|
78 | :conditions => { :fixed_version_id => nil} | |||
|
79 | } | |||
|
80 | } | |||
|
81 | ||||
|
82 | named_scope :with_query, lambda {|query| | |||
|
83 | { | |||
|
84 | :conditions => Query.merge_conditions(query.statement) | |||
|
85 | } | |||
|
86 | } | |||
69 |
|
87 | |||
70 | before_create :default_assign |
|
88 | before_create :default_assign | |
71 | before_save :reschedule_following_issues, :close_duplicates, :update_done_ratio_from_issue_status |
|
89 | before_save :reschedule_following_issues, :close_duplicates, :update_done_ratio_from_issue_status | |
@@ -357,6 +375,13 class Issue < ActiveRecord::Base | |||||
357 | def overdue? |
|
375 | def overdue? | |
358 | !due_date.nil? && (due_date < Date.today) && !status.is_closed? |
|
376 | !due_date.nil? && (due_date < Date.today) && !status.is_closed? | |
359 | end |
|
377 | end | |
|
378 | ||||
|
379 | # Is the amount of work done less than it should for the due date | |||
|
380 | def behind_schedule? | |||
|
381 | return false if start_date.nil? || due_date.nil? | |||
|
382 | done_date = start_date + ((due_date - start_date+1)* done_ratio/100).floor | |||
|
383 | return done_date <= Date.today | |||
|
384 | end | |||
360 |
|
385 | |||
361 | # Users the issue can be assigned to |
|
386 | # Users the issue can be assigned to | |
362 | def assignable_users |
|
387 | def assignable_users |
@@ -412,6 +412,50 class Project < ActiveRecord::Base | |||||
412 | def short_description(length = 255) |
|
412 | def short_description(length = 255) | |
413 | description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description |
|
413 | description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description | |
414 | end |
|
414 | end | |
|
415 | ||||
|
416 | # The earliest start date of a project, based on it's issues and versions | |||
|
417 | def start_date | |||
|
418 | if module_enabled?(:issue_tracking) | |||
|
419 | [ | |||
|
420 | issues.minimum('start_date'), | |||
|
421 | shared_versions.collect(&:effective_date), | |||
|
422 | shared_versions.collect {|v| v.fixed_issues.minimum('start_date')} | |||
|
423 | ].flatten.compact.min | |||
|
424 | end | |||
|
425 | end | |||
|
426 | ||||
|
427 | # The latest due date of an issue or version | |||
|
428 | def due_date | |||
|
429 | if module_enabled?(:issue_tracking) | |||
|
430 | [ | |||
|
431 | issues.maximum('due_date'), | |||
|
432 | shared_versions.collect(&:effective_date), | |||
|
433 | shared_versions.collect {|v| v.fixed_issues.maximum('due_date')} | |||
|
434 | ].flatten.compact.max | |||
|
435 | end | |||
|
436 | end | |||
|
437 | ||||
|
438 | def overdue? | |||
|
439 | active? && !due_date.nil? && (due_date < Date.today) | |||
|
440 | end | |||
|
441 | ||||
|
442 | # Returns the percent completed for this project, based on the | |||
|
443 | # progress on it's versions. | |||
|
444 | def completed_percent(options={:include_subprojects => false}) | |||
|
445 | if options.delete(:include_subprojects) | |||
|
446 | total = self_and_descendants.collect(&:completed_percent).sum | |||
|
447 | ||||
|
448 | total / self_and_descendants.count | |||
|
449 | else | |||
|
450 | if versions.count > 0 | |||
|
451 | total = versions.collect(&:completed_pourcent).sum | |||
|
452 | ||||
|
453 | total / versions.count | |||
|
454 | else | |||
|
455 | 100 | |||
|
456 | end | |||
|
457 | end | |||
|
458 | end | |||
415 |
|
459 | |||
416 | # Return true if this project is allowed to do the specified action. |
|
460 | # Return true if this project is allowed to do the specified action. | |
417 | # action can be: |
|
461 | # action can be: |
@@ -73,6 +73,18 class Version < ActiveRecord::Base | |||||
73 | def completed? |
|
73 | def completed? | |
74 | effective_date && (effective_date <= Date.today) && (open_issues_count == 0) |
|
74 | effective_date && (effective_date <= Date.today) && (open_issues_count == 0) | |
75 | end |
|
75 | end | |
|
76 | ||||
|
77 | def behind_schedule? | |||
|
78 | if completed_pourcent == 100 | |||
|
79 | return false | |||
|
80 | elsif due_date && fixed_issues.present? && fixed_issues.minimum('start_date') # TODO: should use #start_date but that method is wrong... | |||
|
81 | start_date = fixed_issues.minimum('start_date') | |||
|
82 | done_date = start_date + ((due_date - start_date+1)* completed_pourcent/100).floor | |||
|
83 | return done_date <= Date.today | |||
|
84 | else | |||
|
85 | false # No issues so it's not late | |||
|
86 | end | |||
|
87 | end | |||
76 |
|
88 | |||
77 | # Returns the completion percentage of this version based on the amount of open/closed issues |
|
89 | # Returns the completion percentage of this version based on the amount of open/closed issues | |
78 | # and the time spent on the open issues. |
|
90 | # and the time spent on the open issues. |
@@ -1,3 +1,4 | |||||
|
1 | <% @gantt.view = self %> | |||
1 | <h2><%= l(:label_gantt) %></h2> |
|
2 | <h2><%= l(:label_gantt) %></h2> | |
2 |
|
3 | |||
3 | <% 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 %> | |
@@ -55,11 +56,12 if @gantt.zoom >1 | |||||
55 | end |
|
56 | end | |
56 | end |
|
57 | end | |
57 |
|
58 | |||
|
59 | # Width of the entire chart | |||
58 | g_width = (@gantt.date_to - @gantt.date_from + 1)*zoom |
|
60 | g_width = (@gantt.date_to - @gantt.date_from + 1)*zoom | |
59 | g_height = [(20 * @gantt.events.length + 6)+150, 206].max |
|
61 | # Collect the number of issues on Versions | |
|
62 | g_height = [(20 * (@gantt.number_of_rows + 6))+150, 206].max | |||
60 | t_height = g_height + headers_height |
|
63 | t_height = g_height + headers_height | |
61 | %> |
|
64 | %> | |
62 |
|
||||
63 | <table width="100%" style="border:0; border-collapse: collapse;"> |
|
65 | <table width="100%" style="border:0; border-collapse: collapse;"> | |
64 | <tr> |
|
66 | <tr> | |
65 | <td style="width:<%= subject_width %>px; padding:0px;"> |
|
67 | <td style="width:<%= subject_width %>px; padding:0px;"> | |
@@ -67,26 +69,10 t_height = g_height + headers_height | |||||
67 | <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;"> | |
68 | <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> | |
69 | <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> | |
70 | <% |
|
72 | <% top = headers_height + 8 %> | |
71 | # |
|
73 | ||
72 | # Tasks subjects |
|
74 | <%= @gantt.subjects(:headers_height => headers_height, :top => top, :g_width => g_width) %> | |
73 | # |
|
75 | ||
74 | top = headers_height + 8 |
|
|||
75 | @gantt.events.each do |i| |
|
|||
76 | left = 4 + (i.is_a?(Issue) ? i.level * 16 : 0) |
|
|||
77 | %> |
|
|||
78 | <div style="position: absolute;line-height:1.2em;height:16px;top:<%= top %>px;left:<%= left %>px;overflow:hidden;"><small> |
|
|||
79 | <% if i.is_a? Issue %> |
|
|||
80 | <%= h("#{i.project} -") unless @project && @project == i.project %> |
|
|||
81 | <%= link_to_issue i %> |
|
|||
82 | <% else %> |
|
|||
83 | <span class="icon icon-package"> |
|
|||
84 | <%= link_to_version i %> |
|
|||
85 | </span> |
|
|||
86 | <% end %> |
|
|||
87 | </small></div> |
|
|||
88 | <% top = top + 20 |
|
|||
89 | end %> |
|
|||
90 | </div> |
|
76 | </div> | |
91 | </td> |
|
77 | </td> | |
92 | <td> |
|
78 | <td> | |
@@ -164,53 +150,9 if show_days | |||||
164 | end |
|
150 | end | |
165 | end %> |
|
151 | end %> | |
166 |
|
152 | |||
167 | <% |
|
153 | <% top = headers_height + 10 %> | |
168 | # |
|
154 | ||
169 | # Tasks |
|
155 | <%= @gantt.lines(:top => top, :zoom => zoom, :g_width => g_width ) %> | |
170 | # |
|
|||
171 | top = headers_height + 10 |
|
|||
172 | @gantt.events.each do |i| |
|
|||
173 | if i.is_a? Issue |
|
|||
174 | i_start_date = (i.start_date >= @gantt.date_from ? i.start_date : @gantt.date_from ) |
|
|||
175 | i_end_date = (i.due_before <= @gantt.date_to ? i.due_before : @gantt.date_to ) |
|
|||
176 |
|
||||
177 | i_done_date = i.start_date + ((i.due_before - i.start_date+1)*i.done_ratio/100).floor |
|
|||
178 | i_done_date = (i_done_date <= @gantt.date_from ? @gantt.date_from : i_done_date ) |
|
|||
179 | i_done_date = (i_done_date >= @gantt.date_to ? @gantt.date_to : i_done_date ) |
|
|||
180 |
|
||||
181 | i_late_date = [i_end_date, Date.today].min if i_start_date < Date.today |
|
|||
182 |
|
||||
183 | i_left = ((i_start_date - @gantt.date_from)*zoom).floor |
|
|||
184 | i_width = ((i_end_date - i_start_date + 1)*zoom).floor - 2 # total width of the issue (- 2 for left and right borders) |
|
|||
185 | d_width = ((i_done_date - i_start_date)*zoom).floor - 2 # done width |
|
|||
186 | l_width = i_late_date ? ((i_late_date - i_start_date+1)*zoom).floor - 2 : 0 # delay width |
|
|||
187 | css = "task " + (i.leaf? ? 'leaf' : 'parent') |
|
|||
188 | %> |
|
|||
189 | <div style="top:<%= top %>px;left:<%= i_left %>px;width:<%= i_width %>px;" class="<%= css %> task_todo"><div class="left"></div> <div class="right"></div></div> |
|
|||
190 | <% if l_width > 0 %> |
|
|||
191 | <div style="top:<%= top %>px;left:<%= i_left %>px;width:<%= l_width %>px;" class="<%= css %> task_late"> </div> |
|
|||
192 | <% end %> |
|
|||
193 | <% if d_width > 0 %> |
|
|||
194 | <div style="top:<%= top %>px;left:<%= i_left %>px;width:<%= d_width %>px;" class="<%= css %> task_done"> </div> |
|
|||
195 | <% end %> |
|
|||
196 | <div style="top:<%= top %>px;left:<%= i_left + i_width + 8 %>px;background:#fff;" class="<%= css %>"> |
|
|||
197 | <%= i.status.name %> |
|
|||
198 | <%= (i.done_ratio).to_i %>% |
|
|||
199 | </div> |
|
|||
200 | <div class="tooltip" style="position: absolute;top:<%= top %>px;left:<%= i_left %>px;width:<%= i_width %>px;height:12px;"> |
|
|||
201 | <span class="tip"> |
|
|||
202 | <%= render_issue_tooltip i %> |
|
|||
203 | </span></div> |
|
|||
204 | <% else |
|
|||
205 | i_left = ((i.start_date - @gantt.date_from)*zoom).floor |
|
|||
206 | %> |
|
|||
207 | <div style="top:<%= top %>px;left:<%= i_left %>px;width:15px;" class="task milestone"> </div> |
|
|||
208 | <div style="top:<%= top %>px;left:<%= i_left + 12 %>px;background:#fff;" class="task"> |
|
|||
209 | <strong><%= format_version_name i %></strong> |
|
|||
210 | </div> |
|
|||
211 | <% end %> |
|
|||
212 | <% top = top + 20 |
|
|||
213 | end %> |
|
|||
214 |
|
156 | |||
215 | <% |
|
157 | <% | |
216 | # |
|
158 | # |
@@ -184,7 +184,7 module Redmine | |||||
184 | end |
|
184 | end | |
185 | pdf.Output |
|
185 | pdf.Output | |
186 | end |
|
186 | end | |
187 |
|
187 | |||
188 | # Returns a PDF string of a single issue |
|
188 | # Returns a PDF string of a single issue | |
189 | def issue_to_pdf(issue) |
|
189 | def issue_to_pdf(issue) | |
190 | pdf = IFPDF.new(current_language) |
|
190 | pdf = IFPDF.new(current_language) | |
@@ -208,7 +208,7 module Redmine | |||||
208 | pdf.SetFontStyle('',9) |
|
208 | pdf.SetFontStyle('',9) | |
209 | pdf.Cell(60,5, issue.priority.to_s,"RT") |
|
209 | pdf.Cell(60,5, issue.priority.to_s,"RT") | |
210 | pdf.Ln |
|
210 | pdf.Ln | |
211 |
|
|
211 | ||
212 | pdf.SetFontStyle('B',9) |
|
212 | pdf.SetFontStyle('B',9) | |
213 | pdf.Cell(35,5, l(:field_author) + ":","L") |
|
213 | pdf.Cell(35,5, l(:field_author) + ":","L") | |
214 | pdf.SetFontStyle('',9) |
|
214 | pdf.SetFontStyle('',9) | |
@@ -238,14 +238,14 module Redmine | |||||
238 | pdf.SetFontStyle('',9) |
|
238 | pdf.SetFontStyle('',9) | |
239 | pdf.Cell(60,5, format_date(issue.due_date),"RB") |
|
239 | pdf.Cell(60,5, format_date(issue.due_date),"RB") | |
240 | pdf.Ln |
|
240 | pdf.Ln | |
241 |
|
|
241 | ||
242 | for custom_value in issue.custom_field_values |
|
242 | for custom_value in issue.custom_field_values | |
243 | pdf.SetFontStyle('B',9) |
|
243 | pdf.SetFontStyle('B',9) | |
244 | pdf.Cell(35,5, custom_value.custom_field.name + ":","L") |
|
244 | pdf.Cell(35,5, custom_value.custom_field.name + ":","L") | |
245 | pdf.SetFontStyle('',9) |
|
245 | pdf.SetFontStyle('',9) | |
246 | pdf.MultiCell(155,5, (show_value custom_value),"R") |
|
246 | pdf.MultiCell(155,5, (show_value custom_value),"R") | |
247 | end |
|
247 | end | |
248 |
|
|
248 | ||
249 | pdf.SetFontStyle('B',9) |
|
249 | pdf.SetFontStyle('B',9) | |
250 | pdf.Cell(35,5, l(:field_subject) + ":","LTB") |
|
250 | pdf.Cell(35,5, l(:field_subject) + ":","LTB") | |
251 | pdf.SetFontStyle('',9) |
|
251 | pdf.SetFontStyle('',9) | |
@@ -311,187 +311,7 module Redmine | |||||
311 | end |
|
311 | end | |
312 | pdf.Output |
|
312 | pdf.Output | |
313 | end |
|
313 | end | |
314 |
|
314 | |||
315 | # Returns a PDF string of a gantt chart |
|
|||
316 | def gantt_to_pdf(gantt, project) |
|
|||
317 | pdf = IFPDF.new(current_language) |
|
|||
318 | pdf.SetTitle("#{l(:label_gantt)} #{project}") |
|
|||
319 | pdf.AliasNbPages |
|
|||
320 | pdf.footer_date = format_date(Date.today) |
|
|||
321 | pdf.AddPage("L") |
|
|||
322 | pdf.SetFontStyle('B',12) |
|
|||
323 | pdf.SetX(15) |
|
|||
324 | pdf.Cell(70, 20, project.to_s) |
|
|||
325 | pdf.Ln |
|
|||
326 | pdf.SetFontStyle('B',9) |
|
|||
327 |
|
||||
328 | subject_width = 100 |
|
|||
329 | header_heigth = 5 |
|
|||
330 |
|
||||
331 | headers_heigth = header_heigth |
|
|||
332 | show_weeks = false |
|
|||
333 | show_days = false |
|
|||
334 |
|
||||
335 | if gantt.months < 7 |
|
|||
336 | show_weeks = true |
|
|||
337 | headers_heigth = 2*header_heigth |
|
|||
338 | if gantt.months < 3 |
|
|||
339 | show_days = true |
|
|||
340 | headers_heigth = 3*header_heigth |
|
|||
341 | end |
|
|||
342 | end |
|
|||
343 |
|
||||
344 | g_width = 280 - subject_width |
|
|||
345 | zoom = (g_width) / (gantt.date_to - gantt.date_from + 1) |
|
|||
346 | g_height = 120 |
|
|||
347 | t_height = g_height + headers_heigth |
|
|||
348 |
|
||||
349 | y_start = pdf.GetY |
|
|||
350 |
|
||||
351 | # Months headers |
|
|||
352 | month_f = gantt.date_from |
|
|||
353 | left = subject_width |
|
|||
354 | height = header_heigth |
|
|||
355 | gantt.months.times do |
|
|||
356 | width = ((month_f >> 1) - month_f) * zoom |
|
|||
357 | pdf.SetY(y_start) |
|
|||
358 | pdf.SetX(left) |
|
|||
359 | pdf.Cell(width, height, "#{month_f.year}-#{month_f.month}", "LTR", 0, "C") |
|
|||
360 | left = left + width |
|
|||
361 | month_f = month_f >> 1 |
|
|||
362 | end |
|
|||
363 |
|
||||
364 | # Weeks headers |
|
|||
365 | if show_weeks |
|
|||
366 | left = subject_width |
|
|||
367 | height = header_heigth |
|
|||
368 | if gantt.date_from.cwday == 1 |
|
|||
369 | # gantt.date_from is monday |
|
|||
370 | week_f = gantt.date_from |
|
|||
371 | else |
|
|||
372 | # find next monday after gantt.date_from |
|
|||
373 | week_f = gantt.date_from + (7 - gantt.date_from.cwday + 1) |
|
|||
374 | width = (7 - gantt.date_from.cwday + 1) * zoom-1 |
|
|||
375 | pdf.SetY(y_start + header_heigth) |
|
|||
376 | pdf.SetX(left) |
|
|||
377 | pdf.Cell(width + 1, height, "", "LTR") |
|
|||
378 | left = left + width+1 |
|
|||
379 | end |
|
|||
380 | while week_f <= gantt.date_to |
|
|||
381 | width = (week_f + 6 <= gantt.date_to) ? 7 * zoom : (gantt.date_to - week_f + 1) * zoom |
|
|||
382 | pdf.SetY(y_start + header_heigth) |
|
|||
383 | pdf.SetX(left) |
|
|||
384 | pdf.Cell(width, height, (width >= 5 ? week_f.cweek.to_s : ""), "LTR", 0, "C") |
|
|||
385 | left = left + width |
|
|||
386 | week_f = week_f+7 |
|
|||
387 | end |
|
|||
388 | end |
|
|||
389 |
|
||||
390 | # Days headers |
|
|||
391 | if show_days |
|
|||
392 | left = subject_width |
|
|||
393 | height = header_heigth |
|
|||
394 | wday = gantt.date_from.cwday |
|
|||
395 | pdf.SetFontStyle('B',7) |
|
|||
396 | (gantt.date_to - gantt.date_from + 1).to_i.times do |
|
|||
397 | width = zoom |
|
|||
398 | pdf.SetY(y_start + 2 * header_heigth) |
|
|||
399 | pdf.SetX(left) |
|
|||
400 | pdf.Cell(width, height, day_name(wday).first, "LTR", 0, "C") |
|
|||
401 | left = left + width |
|
|||
402 | wday = wday + 1 |
|
|||
403 | wday = 1 if wday > 7 |
|
|||
404 | end |
|
|||
405 | end |
|
|||
406 |
|
||||
407 | pdf.SetY(y_start) |
|
|||
408 | pdf.SetX(15) |
|
|||
409 | pdf.Cell(subject_width+g_width-15, headers_heigth, "", 1) |
|
|||
410 |
|
||||
411 | # Tasks |
|
|||
412 | top = headers_heigth + y_start |
|
|||
413 | pdf.SetFontStyle('B',7) |
|
|||
414 | gantt.events.each do |i| |
|
|||
415 | pdf.SetY(top) |
|
|||
416 | pdf.SetX(15) |
|
|||
417 |
|
||||
418 | text = "" |
|
|||
419 | if i.is_a? Issue |
|
|||
420 | text = "#{i.tracker} #{i.id}: #{i.subject}" |
|
|||
421 | else |
|
|||
422 | text = i.name |
|
|||
423 | end |
|
|||
424 | text = "#{i.project} - #{text}" unless project && project == i.project |
|
|||
425 | pdf.Cell(subject_width-15, 5, text, "LR") |
|
|||
426 |
|
||||
427 | pdf.SetY(top + 0.2) |
|
|||
428 | pdf.SetX(subject_width) |
|
|||
429 | pdf.SetFillColor(255, 255, 255) |
|
|||
430 | pdf.Cell(g_width, 4.6, "", "LR", 0, "", 1) |
|
|||
431 | pdf.SetY(top+1.5) |
|
|||
432 |
|
||||
433 | if i.is_a? Issue |
|
|||
434 | i_start_date = (i.start_date >= gantt.date_from ? i.start_date : gantt.date_from ) |
|
|||
435 | i_end_date = (i.due_before <= gantt.date_to ? i.due_before : gantt.date_to ) |
|
|||
436 |
|
||||
437 | i_done_date = i.start_date + ((i.due_before - i.start_date+1)*i.done_ratio/100).floor |
|
|||
438 | i_done_date = (i_done_date <= gantt.date_from ? gantt.date_from : i_done_date ) |
|
|||
439 | i_done_date = (i_done_date >= gantt.date_to ? gantt.date_to : i_done_date ) |
|
|||
440 |
|
||||
441 | i_late_date = [i_end_date, Date.today].min if i_start_date < Date.today |
|
|||
442 |
|
||||
443 | i_left = ((i_start_date - gantt.date_from)*zoom) |
|
|||
444 | i_width = ((i_end_date - i_start_date + 1)*zoom) |
|
|||
445 | d_width = ((i_done_date - i_start_date)*zoom) |
|
|||
446 | l_width = ((i_late_date - i_start_date+1)*zoom) if i_late_date |
|
|||
447 | l_width ||= 0 |
|
|||
448 |
|
||||
449 | pdf.SetX(subject_width + i_left) |
|
|||
450 | pdf.SetFillColor(200,200,200) |
|
|||
451 | pdf.Cell(i_width, 2, "", 0, 0, "", 1) |
|
|||
452 |
|
||||
453 | if l_width > 0 |
|
|||
454 | pdf.SetY(top+1.5) |
|
|||
455 | pdf.SetX(subject_width + i_left) |
|
|||
456 | pdf.SetFillColor(255,100,100) |
|
|||
457 | pdf.Cell(l_width, 2, "", 0, 0, "", 1) |
|
|||
458 | end |
|
|||
459 | if d_width > 0 |
|
|||
460 | pdf.SetY(top+1.5) |
|
|||
461 | pdf.SetX(subject_width + i_left) |
|
|||
462 | pdf.SetFillColor(100,100,255) |
|
|||
463 | pdf.Cell(d_width, 2, "", 0, 0, "", 1) |
|
|||
464 | end |
|
|||
465 |
|
||||
466 | pdf.SetY(top+1.5) |
|
|||
467 | pdf.SetX(subject_width + i_left + i_width) |
|
|||
468 | pdf.Cell(30, 2, "#{i.status} #{i.done_ratio}%") |
|
|||
469 | else |
|
|||
470 | i_left = ((i.start_date - gantt.date_from)*zoom) |
|
|||
471 |
|
||||
472 | pdf.SetX(subject_width + i_left) |
|
|||
473 | pdf.SetFillColor(50,200,50) |
|
|||
474 | pdf.Cell(2, 2, "", 0, 0, "", 1) |
|
|||
475 |
|
||||
476 | pdf.SetY(top+1.5) |
|
|||
477 | pdf.SetX(subject_width + i_left + 3) |
|
|||
478 | pdf.Cell(30, 2, "#{i.name}") |
|
|||
479 | end |
|
|||
480 |
|
||||
481 | top = top + 5 |
|
|||
482 | pdf.SetDrawColor(200, 200, 200) |
|
|||
483 | pdf.Line(15, top, subject_width+g_width, top) |
|
|||
484 | if pdf.GetY() > 180 |
|
|||
485 | pdf.AddPage("L") |
|
|||
486 | top = 20 |
|
|||
487 | pdf.Line(15, top, subject_width+g_width, top) |
|
|||
488 | end |
|
|||
489 | pdf.SetDrawColor(0, 0, 0) |
|
|||
490 | end |
|
|||
491 |
|
||||
492 | pdf.Line(15, top, subject_width+g_width, top) |
|
|||
493 | pdf.Output |
|
|||
494 | end |
|
|||
495 | end |
|
315 | end | |
496 | end |
|
316 | end | |
497 | end |
|
317 | end |
This diff has been collapsed as it changes many lines, (891 lines changed) Show them Hide them | |||||
@@ -19,11 +19,28 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 | attr_reader :year_from, :month_from, :date_from, :date_to, :zoom, :months, :events |
|
22 | include ERB::Util | |
23 |
|
23 | include Redmine::I18n | ||
|
24 | ||||
|
25 | # :nodoc: | |||
|
26 | # Some utility methods for the PDF export | |||
|
27 | class PDF | |||
|
28 | MaxCharactorsForSubject = 45 | |||
|
29 | TotalWidth = 280 | |||
|
30 | LeftPaneWidth = 100 | |||
|
31 | ||||
|
32 | def self.right_pane_width | |||
|
33 | TotalWidth - LeftPaneWidth | |||
|
34 | end | |||
|
35 | end | |||
|
36 | ||||
|
37 | attr_reader :year_from, :month_from, :date_from, :date_to, :zoom, :months | |||
|
38 | attr_accessor :query | |||
|
39 | attr_accessor :project | |||
|
40 | attr_accessor :view | |||
|
41 | ||||
24 | def initialize(options={}) |
|
42 | def initialize(options={}) | |
25 | options = options.dup |
|
43 | options = options.dup | |
26 | @events = [] |
|
|||
27 |
|
44 | |||
28 | if options[:year] && options[:year].to_i >0 |
|
45 | if options[:year] && options[:year].to_i >0 | |
29 | @year_from = options[:year].to_i |
|
46 | @year_from = options[:year].to_i | |
@@ -52,31 +69,6 module Redmine | |||||
52 | @date_to = (@date_from >> @months) - 1 |
|
69 | @date_to = (@date_from >> @months) - 1 | |
53 | end |
|
70 | end | |
54 |
|
71 | |||
55 |
|
||||
56 | def events=(e) |
|
|||
57 | @events = e |
|
|||
58 | # Adds all ancestors |
|
|||
59 | root_ids = e.select {|i| i.is_a?(Issue) && i.parent_id? }.collect(&:root_id).uniq |
|
|||
60 | if root_ids.any? |
|
|||
61 | # Retrieves all nodes |
|
|||
62 | parents = Issue.find_all_by_root_id(root_ids, :conditions => ["rgt - lft > 1"]) |
|
|||
63 | # Only add ancestors |
|
|||
64 | @events += parents.select {|p| @events.detect {|i| i.is_a?(Issue) && p.is_ancestor_of?(i)}} |
|
|||
65 | end |
|
|||
66 | @events.uniq! |
|
|||
67 | # Sort issues by hierarchy and start dates |
|
|||
68 | @events.sort! {|x,y| |
|
|||
69 | if x.is_a?(Issue) && y.is_a?(Issue) |
|
|||
70 | gantt_issue_compare(x, y, @events) |
|
|||
71 | else |
|
|||
72 | gantt_start_compare(x, y) |
|
|||
73 | end |
|
|||
74 | } |
|
|||
75 | # Removes issues that have no start or end date |
|
|||
76 | @events.reject! {|i| i.is_a?(Issue) && (i.start_date.nil? || i.due_before.nil?) } |
|
|||
77 | @events |
|
|||
78 | end |
|
|||
79 |
|
||||
80 | def params |
|
72 | def params | |
81 | { :zoom => zoom, :year => year_from, :month => month_from, :months => months } |
|
73 | { :zoom => zoom, :year => year_from, :month => month_from, :months => months } | |
82 | end |
|
74 | end | |
@@ -88,10 +80,652 module Redmine | |||||
88 | def params_next |
|
80 | def params_next | |
89 | { :year => (date_from >> months).year, :month => (date_from >> months).month, :zoom => zoom, :months => months } |
|
81 | { :year => (date_from >> months).year, :month => (date_from >> months).month, :zoom => zoom, :months => months } | |
90 | end |
|
82 | end | |
91 |
|
83 | |||
|
84 | ### Extracted from the HTML view/helpers | |||
|
85 | # Returns the number of rows that will be rendered on the Gantt chart | |||
|
86 | def number_of_rows | |||
|
87 | if @project | |||
|
88 | return number_of_rows_on_project(@project) | |||
|
89 | else | |||
|
90 | Project.roots.inject(0) do |total, project| | |||
|
91 | total += number_of_rows_on_project(project) | |||
|
92 | end | |||
|
93 | end | |||
|
94 | end | |||
|
95 | ||||
|
96 | # Returns the number of rows that will be used to list a project on | |||
|
97 | # the Gantt chart. This will recurse for each subproject. | |||
|
98 | def number_of_rows_on_project(project) | |||
|
99 | # Remove the project requirement for Versions because it will | |||
|
100 | # restrict issues to only be on the current project. This | |||
|
101 | # ends up missing issues which are assigned to shared versions. | |||
|
102 | @query.project = nil if @query.project | |||
|
103 | ||||
|
104 | # One Root project | |||
|
105 | count = 1 | |||
|
106 | # Issues without a Version | |||
|
107 | count += project.issues.for_gantt.without_version.with_query(@query).count | |||
|
108 | ||||
|
109 | # Versions | |||
|
110 | count += project.versions.count | |||
|
111 | ||||
|
112 | # Issues on the Versions | |||
|
113 | project.versions.each do |version| | |||
|
114 | count += version.fixed_issues.for_gantt.with_query(@query).count | |||
|
115 | end | |||
|
116 | ||||
|
117 | # Subprojects | |||
|
118 | project.children.each do |subproject| | |||
|
119 | count += number_of_rows_on_project(subproject) | |||
|
120 | end | |||
|
121 | ||||
|
122 | count | |||
|
123 | end | |||
|
124 | ||||
|
125 | # Renders the subjects of the Gantt chart, the left side. | |||
|
126 | def subjects(options={}) | |||
|
127 | options = {:indent => 4, :render => :subject, :format => :html}.merge(options) | |||
|
128 | ||||
|
129 | output = '' | |||
|
130 | if @project | |||
|
131 | output << render_project(@project, options) | |||
|
132 | else | |||
|
133 | Project.roots.each do |project| | |||
|
134 | output << render_project(project, options) | |||
|
135 | end | |||
|
136 | end | |||
|
137 | ||||
|
138 | output | |||
|
139 | end | |||
|
140 | ||||
|
141 | # Renders the lines of the Gantt chart, the right side | |||
|
142 | def lines(options={}) | |||
|
143 | options = {:indent => 4, :render => :line, :format => :html}.merge(options) | |||
|
144 | output = '' | |||
|
145 | ||||
|
146 | if @project | |||
|
147 | output << render_project(@project, options) | |||
|
148 | else | |||
|
149 | Project.roots.each do |project| | |||
|
150 | output << render_project(project, options) | |||
|
151 | end | |||
|
152 | end | |||
|
153 | ||||
|
154 | output | |||
|
155 | end | |||
|
156 | ||||
|
157 | def render_project(project, options={}) | |||
|
158 | options[:top] = 0 unless options.key? :top | |||
|
159 | options[:indent_increment] = 20 unless options.key? :indent_increment | |||
|
160 | options[:top_increment] = 20 unless options.key? :top_increment | |||
|
161 | ||||
|
162 | output = '' | |||
|
163 | # Project Header | |||
|
164 | project_header = if options[:render] == :subject | |||
|
165 | subject_for_project(project, options) | |||
|
166 | else | |||
|
167 | # :line | |||
|
168 | line_for_project(project, options) | |||
|
169 | end | |||
|
170 | output << project_header if options[:format] == :html | |||
|
171 | ||||
|
172 | options[:top] += options[:top_increment] | |||
|
173 | options[:indent] += options[:indent_increment] | |||
|
174 | ||||
|
175 | # Second, Issues without a version | |||
|
176 | issues = project.issues.for_gantt.without_version.with_query(@query) | |||
|
177 | if issues | |||
|
178 | issue_rendering = render_issues(issues, options) | |||
|
179 | output << issue_rendering if options[:format] == :html | |||
|
180 | end | |||
|
181 | ||||
|
182 | # Third, Versions | |||
|
183 | project.versions.sort.each do |version| | |||
|
184 | version_rendering = render_version(version, options) | |||
|
185 | output << version_rendering if options[:format] == :html | |||
|
186 | end | |||
|
187 | ||||
|
188 | # Fourth, subprojects | |||
|
189 | project.children.each do |project| | |||
|
190 | subproject_rendering = render_project(project, options) | |||
|
191 | output << subproject_rendering if options[:format] == :html | |||
|
192 | end | |||
|
193 | ||||
|
194 | # Remove indent to hit the next sibling | |||
|
195 | options[:indent] -= options[:indent_increment] | |||
|
196 | ||||
|
197 | output | |||
|
198 | end | |||
|
199 | ||||
|
200 | def render_issues(issues, options={}) | |||
|
201 | output = '' | |||
|
202 | issues.each do |i| | |||
|
203 | issue_rendering = if options[:render] == :subject | |||
|
204 | subject_for_issue(i, options) | |||
|
205 | else | |||
|
206 | # :line | |||
|
207 | line_for_issue(i, options) | |||
|
208 | end | |||
|
209 | output << issue_rendering if options[:format] == :html | |||
|
210 | options[:top] += options[:top_increment] | |||
|
211 | end | |||
|
212 | output | |||
|
213 | end | |||
|
214 | ||||
|
215 | def render_version(version, options={}) | |||
|
216 | output = '' | |||
|
217 | # Version header | |||
|
218 | version_rendering = if options[:render] == :subject | |||
|
219 | subject_for_version(version, options) | |||
|
220 | else | |||
|
221 | # :line | |||
|
222 | line_for_version(version, options) | |||
|
223 | end | |||
|
224 | ||||
|
225 | output << version_rendering if options[:format] == :html | |||
|
226 | ||||
|
227 | options[:top] += options[:top_increment] | |||
|
228 | ||||
|
229 | # Remove the project requirement for Versions because it will | |||
|
230 | # restrict issues to only be on the current project. This | |||
|
231 | # ends up missing issues which are assigned to shared versions. | |||
|
232 | @query.project = nil if @query.project | |||
|
233 | ||||
|
234 | issues = version.fixed_issues.for_gantt.with_query(@query) | |||
|
235 | if issues | |||
|
236 | # Indent issues | |||
|
237 | options[:indent] += options[:indent_increment] | |||
|
238 | output << render_issues(issues, options) | |||
|
239 | options[:indent] -= options[:indent_increment] | |||
|
240 | end | |||
|
241 | ||||
|
242 | output | |||
|
243 | end | |||
|
244 | ||||
|
245 | def subject_for_project(project, options) | |||
|
246 | case options[:format] | |||
|
247 | when :html | |||
|
248 | output = '' | |||
|
249 | ||||
|
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> " | |||
|
251 | if project.is_a? Project | |||
|
252 | output << "<span class='icon icon-projects #{project.overdue? ? 'project-overdue' : ''}'>" | |||
|
253 | output << view.link_to_project(project) | |||
|
254 | output << '</span>' | |||
|
255 | else | |||
|
256 | ActiveRecord::Base.logger.debug "Gantt#subject_for_project was not given a project" | |||
|
257 | '' | |||
|
258 | end | |||
|
259 | output << "</small></div>" | |||
|
260 | ||||
|
261 | output | |||
|
262 | when :image | |||
|
263 | ||||
|
264 | options[:image].fill('black') | |||
|
265 | options[:image].stroke('transparent') | |||
|
266 | options[:image].stroke_width(1) | |||
|
267 | options[:image].text(options[:indent], options[:top] + 2, project.name) | |||
|
268 | when :pdf | |||
|
269 | options[:pdf].SetY(options[:top]) | |||
|
270 | options[:pdf].SetX(15) | |||
|
271 | ||||
|
272 | 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") | |||
|
274 | ||||
|
275 | options[:pdf].SetY(options[:top]) | |||
|
276 | options[:pdf].SetX(options[:subject_width]) | |||
|
277 | options[:pdf].Cell(options[:g_width], 5, "", "LR") | |||
|
278 | end | |||
|
279 | end | |||
|
280 | ||||
|
281 | def line_for_project(project, options) | |||
|
282 | # Skip versions that don't have a start_date | |||
|
283 | if project.is_a?(Project) && project.start_date | |||
|
284 | options[:zoom] ||= 1 | |||
|
285 | options[:g_width] ||= (self.date_to - self.date_from + 1) * options[:zoom] | |||
|
286 | ||||
|
287 | ||||
|
288 | case options[:format] | |||
|
289 | when :html | |||
|
290 | output = '' | |||
|
291 | i_left = ((project.start_date - self.date_from)*options[:zoom]).floor | |||
|
292 | ||||
|
293 | start_date = project.start_date | |||
|
294 | start_date ||= self.date_from | |||
|
295 | start_left = ((start_date - self.date_from)*options[:zoom]).floor | |||
|
296 | ||||
|
297 | 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 | |||
|
299 | 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 ) | |||
|
301 | ||||
|
302 | 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 | |||
|
304 | ||||
|
305 | 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 | |||
|
307 | l_width = i_late_date ? ((i_late_date - start_date+1)*options[:zoom]).floor - 2 : 0 # delay width | |||
|
308 | ||||
|
309 | # Bar graphic | |||
|
310 | ||||
|
311 | # Make sure that negative i_left and i_width don't | |||
|
312 | # overflow the subject | |||
|
313 | 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'> </div>" | |||
|
315 | end | |||
|
316 | ||||
|
317 | 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'> </div>" | |||
|
319 | end | |||
|
320 | 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'> </div>" | |||
|
322 | end | |||
|
323 | ||||
|
324 | ||||
|
325 | # Starting diamond | |||
|
326 | 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'> </div>" | |||
|
328 | output << "<div style='top:#{ options[:top] }px;left:#{ start_left + 12 }px;' class='task label'>" | |||
|
329 | output << "</div>" | |||
|
330 | end | |||
|
331 | ||||
|
332 | # Ending diamond | |||
|
333 | # Don't show items too far ahead | |||
|
334 | 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'> </div>" | |||
|
336 | end | |||
|
337 | ||||
|
338 | # DIsplay the Project name and % | |||
|
339 | if i_end <= options[:g_width] | |||
|
340 | # Display the status even if it's floated off to the left | |||
|
341 | status_px = i_end + 12 # 12px for the diamond | |||
|
342 | status_px = 0 if status_px <= 0 | |||
|
343 | ||||
|
344 | 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>" | |||
|
346 | output << "</div>" | |||
|
347 | end | |||
|
348 | ||||
|
349 | output | |||
|
350 | when :image | |||
|
351 | options[:image].stroke('transparent') | |||
|
352 | i_left = options[:subject_width] + ((project.due_date - self.date_from)*options[:zoom]).floor | |||
|
353 | ||||
|
354 | # Make sure negative i_left doesn't overflow the subject | |||
|
355 | if i_left > options[:subject_width] | |||
|
356 | options[:image].fill('blue') | |||
|
357 | options[:image].rectangle(i_left, options[:top], i_left + 6, options[:top] - 6) | |||
|
358 | options[:image].fill('black') | |||
|
359 | options[:image].text(i_left + 11, options[:top] + 1, project.name) | |||
|
360 | end | |||
|
361 | when :pdf | |||
|
362 | options[:pdf].SetY(options[:top]+1.5) | |||
|
363 | i_left = ((project.due_date - @date_from)*options[:zoom]) | |||
|
364 | ||||
|
365 | # Make sure negative i_left doesn't overflow the subject | |||
|
366 | if i_left > 0 | |||
|
367 | options[:pdf].SetX(options[:subject_width] + i_left) | |||
|
368 | options[:pdf].SetFillColor(50,50,200) | |||
|
369 | options[:pdf].Cell(2, 2, "", 0, 0, "", 1) | |||
|
370 | ||||
|
371 | options[:pdf].SetY(options[:top]+1.5) | |||
|
372 | options[:pdf].SetX(options[:subject_width] + i_left + 3) | |||
|
373 | options[:pdf].Cell(30, 2, "#{project.name}") | |||
|
374 | end | |||
|
375 | end | |||
|
376 | else | |||
|
377 | ActiveRecord::Base.logger.debug "Gantt#line_for_project was not given a project with a start_date" | |||
|
378 | '' | |||
|
379 | end | |||
|
380 | end | |||
|
381 | ||||
|
382 | def subject_for_version(version, options) | |||
|
383 | case options[:format] | |||
|
384 | when :html | |||
|
385 | 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> " | |||
|
387 | if version.is_a? Version | |||
|
388 | output << "<span class='icon icon-package #{version.behind_schedule? ? 'version-behind-schedule' : ''} #{version.overdue? ? 'version-overdue' : ''}'>" | |||
|
389 | output << view.link_to_version(version) | |||
|
390 | output << '</span>' | |||
|
391 | else | |||
|
392 | ActiveRecord::Base.logger.debug "Gantt#subject_for_version was not given a version" | |||
|
393 | '' | |||
|
394 | end | |||
|
395 | output << "</small></div>" | |||
|
396 | ||||
|
397 | output | |||
|
398 | when :image | |||
|
399 | options[:image].fill('black') | |||
|
400 | options[:image].stroke('transparent') | |||
|
401 | options[:image].stroke_width(1) | |||
|
402 | options[:image].text(options[:indent], options[:top] + 2, version.name) | |||
|
403 | when :pdf | |||
|
404 | options[:pdf].SetY(options[:top]) | |||
|
405 | options[:pdf].SetX(15) | |||
|
406 | ||||
|
407 | char_limit = PDF::MaxCharactorsForSubject - options[:indent] | |||
|
408 | options[:pdf].Cell(options[:subject_width]-15, 5, (" " * options[:indent]) +"#{version.name}".sub(/^(.{#{char_limit}}[^\s]*\s).*$/, '\1 (...)'), "LR") | |||
|
409 | ||||
|
410 | options[:pdf].SetY(options[:top]) | |||
|
411 | options[:pdf].SetX(options[:subject_width]) | |||
|
412 | options[:pdf].Cell(options[:g_width], 5, "", "LR") | |||
|
413 | end | |||
|
414 | end | |||
|
415 | ||||
|
416 | def line_for_version(version, options) | |||
|
417 | # Skip versions that don't have a start_date | |||
|
418 | if version.is_a?(Version) && version.start_date | |||
|
419 | options[:zoom] ||= 1 | |||
|
420 | options[:g_width] ||= (self.date_to - self.date_from + 1) * options[:zoom] | |||
|
421 | ||||
|
422 | case options[:format] | |||
|
423 | when :html | |||
|
424 | output = '' | |||
|
425 | i_left = ((version.start_date - self.date_from)*options[:zoom]).floor | |||
|
426 | # TODO: or version.fixed_issues.collect(&:start_date).min | |||
|
427 | start_date = version.fixed_issues.minimum('start_date') if version.fixed_issues.present? | |||
|
428 | start_date ||= self.date_from | |||
|
429 | start_left = ((start_date - self.date_from)*options[:zoom]).floor | |||
|
430 | ||||
|
431 | 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 | |||
|
433 | 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 ) | |||
|
435 | ||||
|
436 | i_late_date = [i_end_date, Date.today].min if start_date < Date.today | |||
|
437 | ||||
|
438 | 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 | |||
|
440 | l_width = i_late_date ? ((i_late_date - start_date+1)*options[:zoom]).floor - 2 : 0 # delay width | |||
|
441 | ||||
|
442 | i_end = ((i_end_date - self.date_from) * options[:zoom]).floor # Ending pixel | |||
|
443 | ||||
|
444 | # Bar graphic | |||
|
445 | ||||
|
446 | # Make sure that negative i_left and i_width don't | |||
|
447 | # overflow the subject | |||
|
448 | 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'> </div>" | |||
|
450 | end | |||
|
451 | 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'> </div>" | |||
|
453 | end | |||
|
454 | 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'> </div>" | |||
|
456 | end | |||
|
457 | ||||
|
458 | ||||
|
459 | # Starting diamond | |||
|
460 | 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'> </div>" | |||
|
462 | output << "<div style='top:#{ options[:top] }px;left:#{ start_left + 12 }px;background:#fff;' class='task'>" | |||
|
463 | output << "</div>" | |||
|
464 | end | |||
|
465 | ||||
|
466 | # Ending diamond | |||
|
467 | # Don't show items too far ahead | |||
|
468 | 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'> </div>" | |||
|
470 | end | |||
|
471 | ||||
|
472 | # Display the Version name and % | |||
|
473 | if i_end <= options[:g_width] | |||
|
474 | # Display the status even if it's floated off to the left | |||
|
475 | status_px = i_end + 12 # 12px for the diamond | |||
|
476 | status_px = 0 if status_px <= 0 | |||
|
477 | ||||
|
478 | 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 | |||
|
480 | output << "<strong>#{h version } #{h version.completed_pourcent.to_i.to_s}%</strong>" | |||
|
481 | output << "</div>" | |||
|
482 | end | |||
|
483 | ||||
|
484 | output | |||
|
485 | when :image | |||
|
486 | options[:image].stroke('transparent') | |||
|
487 | i_left = options[:subject_width] + ((version.start_date - @date_from)*options[:zoom]).floor | |||
|
488 | ||||
|
489 | # Make sure negative i_left doesn't overflow the subject | |||
|
490 | if i_left > options[:subject_width] | |||
|
491 | options[:image].fill('green') | |||
|
492 | options[:image].rectangle(i_left, options[:top], i_left + 6, options[:top] - 6) | |||
|
493 | options[:image].fill('black') | |||
|
494 | options[:image].text(i_left + 11, options[:top] + 1, version.name) | |||
|
495 | end | |||
|
496 | when :pdf | |||
|
497 | options[:pdf].SetY(options[:top]+1.5) | |||
|
498 | i_left = ((version.start_date - @date_from)*options[:zoom]) | |||
|
499 | ||||
|
500 | # Make sure negative i_left doesn't overflow the subject | |||
|
501 | if i_left > 0 | |||
|
502 | options[:pdf].SetX(options[:subject_width] + i_left) | |||
|
503 | options[:pdf].SetFillColor(50,200,50) | |||
|
504 | options[:pdf].Cell(2, 2, "", 0, 0, "", 1) | |||
|
505 | ||||
|
506 | options[:pdf].SetY(options[:top]+1.5) | |||
|
507 | options[:pdf].SetX(options[:subject_width] + i_left + 3) | |||
|
508 | options[:pdf].Cell(30, 2, "#{version.name}") | |||
|
509 | end | |||
|
510 | end | |||
|
511 | else | |||
|
512 | ActiveRecord::Base.logger.debug "Gantt#line_for_version was not given a version with a start_date" | |||
|
513 | '' | |||
|
514 | end | |||
|
515 | end | |||
|
516 | ||||
|
517 | def subject_for_issue(issue, options) | |||
|
518 | case options[:format] | |||
|
519 | when :html | |||
|
520 | output = '' | |||
|
521 | 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> " | |||
|
523 | if issue.is_a? Issue | |||
|
524 | css_classes = [] | |||
|
525 | css_classes << 'issue-overdue' if issue.overdue? | |||
|
526 | css_classes << 'issue-behind-schedule' if issue.behind_schedule? | |||
|
527 | css_classes << 'icon icon-issue' unless Setting.gravatar_enabled? && issue.assigned_to | |||
|
528 | ||||
|
529 | if issue.assigned_to.present? | |||
|
530 | 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) | |||
|
532 | end | |||
|
533 | output << "<span class='#{css_classes.join(' ')}'>" | |||
|
534 | output << view.link_to_issue(issue) | |||
|
535 | output << ":" | |||
|
536 | output << h(issue.subject) | |||
|
537 | output << '</span>' | |||
|
538 | else | |||
|
539 | ActiveRecord::Base.logger.debug "Gantt#subject_for_issue was not given an issue" | |||
|
540 | '' | |||
|
541 | end | |||
|
542 | output << "</small></div>" | |||
|
543 | ||||
|
544 | # Tooltip | |||
|
545 | 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;'>" | |||
|
547 | output << view.render_issue_tooltip(issue) | |||
|
548 | output << "</span>" | |||
|
549 | end | |||
|
550 | ||||
|
551 | output << "</div>" | |||
|
552 | output | |||
|
553 | when :image | |||
|
554 | options[:image].fill('black') | |||
|
555 | options[:image].stroke('transparent') | |||
|
556 | options[:image].stroke_width(1) | |||
|
557 | options[:image].text(options[:indent], options[:top] + 2, issue.subject) | |||
|
558 | when :pdf | |||
|
559 | options[:pdf].SetY(options[:top]) | |||
|
560 | options[:pdf].SetX(15) | |||
|
561 | ||||
|
562 | 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") | |||
|
564 | ||||
|
565 | options[:pdf].SetY(options[:top]) | |||
|
566 | options[:pdf].SetX(options[:subject_width]) | |||
|
567 | options[:pdf].Cell(options[:g_width], 5, "", "LR") | |||
|
568 | end | |||
|
569 | end | |||
|
570 | ||||
|
571 | def line_for_issue(issue, options) | |||
|
572 | # 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 | |||
|
574 | case options[:format] | |||
|
575 | when :html | |||
|
576 | output = '' | |||
|
577 | # Handle nil start_dates, rare but can happen. | |||
|
578 | i_start_date = if issue.start_date && issue.start_date >= self.date_from | |||
|
579 | issue.start_date | |||
|
580 | else | |||
|
581 | self.date_from | |||
|
582 | end | |||
|
583 | ||||
|
584 | 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 | |||
|
586 | 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 ) | |||
|
588 | ||||
|
589 | i_late_date = [i_end_date, Date.today].min if i_start_date < Date.today | |||
|
590 | ||||
|
591 | 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) | |||
|
593 | 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 | |||
|
595 | css = "task " + (issue.leaf? ? 'leaf' : 'parent') | |||
|
596 | ||||
|
597 | # Make sure that negative i_left and i_width don't | |||
|
598 | # overflow the subject | |||
|
599 | if i_width > 0 | |||
|
600 | output << "<div style='top:#{ options[:top] }px;left:#{ i_left }px;width:#{ i_width }px;' class='#{css} task_todo'> </div>" | |||
|
601 | end | |||
|
602 | if l_width > 0 | |||
|
603 | output << "<div style='top:#{ options[:top] }px;left:#{ i_left }px;width:#{ l_width }px;' class='#{css} task_late'> </div>" | |||
|
604 | end | |||
|
605 | if d_width > 0 | |||
|
606 | output<< "<div style='top:#{ options[:top] }px;left:#{ i_left }px;width:#{ d_width }px;' class='#{css} task_done'> </div>" | |||
|
607 | end | |||
|
608 | ||||
|
609 | # Display the status even if it's floated off to the left | |||
|
610 | status_px = i_left + i_width + 5 | |||
|
611 | status_px = 5 if status_px <= 0 | |||
|
612 | ||||
|
613 | output << "<div style='top:#{ options[:top] }px;left:#{ status_px }px;' class='#{css} label issue-name'>" | |||
|
614 | output << issue.status.name | |||
|
615 | output << ' ' | |||
|
616 | output << (issue.done_ratio).to_i.to_s | |||
|
617 | output << "%" | |||
|
618 | output << "</div>" | |||
|
619 | ||||
|
620 | 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">' | |||
|
622 | output << view.render_issue_tooltip(issue) | |||
|
623 | output << "</span></div>" | |||
|
624 | output | |||
|
625 | ||||
|
626 | when :image | |||
|
627 | # Handle nil start_dates, rare but can happen. | |||
|
628 | i_start_date = if issue.start_date && issue.start_date >= @date_from | |||
|
629 | issue.start_date | |||
|
630 | else | |||
|
631 | @date_from | |||
|
632 | end | |||
|
633 | ||||
|
634 | 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 | |||
|
636 | 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 ) | |||
|
638 | i_late_date = [i_end_date, Date.today].min if i_start_date < Date.today | |||
|
639 | ||||
|
640 | 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 | |||
|
642 | 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 | |||
|
644 | ||||
|
645 | ||||
|
646 | # Make sure that negative i_left and i_width don't | |||
|
647 | # overflow the subject | |||
|
648 | if i_width > 0 | |||
|
649 | options[:image].fill('grey') | |||
|
650 | options[:image].rectangle(i_left, options[:top], i_left + i_width, options[:top] - 6) | |||
|
651 | options[:image].fill('red') | |||
|
652 | options[:image].rectangle(i_left, options[:top], i_left + l_width, options[:top] - 6) if l_width > 0 | |||
|
653 | options[:image].fill('blue') | |||
|
654 | options[:image].rectangle(i_left, options[:top], i_left + d_width, options[:top] - 6) if d_width > 0 | |||
|
655 | end | |||
|
656 | ||||
|
657 | # Show the status and % done next to the subject if it overflows | |||
|
658 | options[:image].fill('black') | |||
|
659 | if i_width > 0 | |||
|
660 | options[:image].text(i_left + i_width + 5,options[:top] + 1, "#{issue.status.name} #{issue.done_ratio}%") | |||
|
661 | else | |||
|
662 | options[:image].text(options[:subject_width] + 5,options[:top] + 1, "#{issue.status.name} #{issue.done_ratio}%") | |||
|
663 | end | |||
|
664 | ||||
|
665 | when :pdf | |||
|
666 | options[:pdf].SetY(options[:top]+1.5) | |||
|
667 | # Handle nil start_dates, rare but can happen. | |||
|
668 | i_start_date = if issue.start_date && issue.start_date >= @date_from | |||
|
669 | issue.start_date | |||
|
670 | else | |||
|
671 | @date_from | |||
|
672 | end | |||
|
673 | ||||
|
674 | i_end_date = (issue.due_before <= @date_to ? issue.due_before : @date_to ) | |||
|
675 | ||||
|
676 | 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 ) | |||
|
678 | i_done_date = (i_done_date >= @date_to ? @date_to : i_done_date ) | |||
|
679 | ||||
|
680 | i_late_date = [i_end_date, Date.today].min if i_start_date < Date.today | |||
|
681 | ||||
|
682 | i_left = ((i_start_date - @date_from)*options[:zoom]) | |||
|
683 | i_width = ((i_end_date - i_start_date + 1)*options[:zoom]) | |||
|
684 | 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 | |||
|
686 | l_width ||= 0 | |||
|
687 | ||||
|
688 | # Make sure that negative i_left and i_width don't | |||
|
689 | # overflow the subject | |||
|
690 | if i_width > 0 | |||
|
691 | options[:pdf].SetX(options[:subject_width] + i_left) | |||
|
692 | options[:pdf].SetFillColor(200,200,200) | |||
|
693 | options[:pdf].Cell(i_width, 2, "", 0, 0, "", 1) | |||
|
694 | end | |||
|
695 | ||||
|
696 | if l_width > 0 | |||
|
697 | options[:pdf].SetY(options[:top]+1.5) | |||
|
698 | options[:pdf].SetX(options[:subject_width] + i_left) | |||
|
699 | options[:pdf].SetFillColor(255,100,100) | |||
|
700 | options[:pdf].Cell(l_width, 2, "", 0, 0, "", 1) | |||
|
701 | end | |||
|
702 | if d_width > 0 | |||
|
703 | options[:pdf].SetY(options[:top]+1.5) | |||
|
704 | options[:pdf].SetX(options[:subject_width] + i_left) | |||
|
705 | options[:pdf].SetFillColor(100,100,255) | |||
|
706 | options[:pdf].Cell(d_width, 2, "", 0, 0, "", 1) | |||
|
707 | end | |||
|
708 | ||||
|
709 | options[:pdf].SetY(options[:top]+1.5) | |||
|
710 | ||||
|
711 | # Make sure that negative i_left and i_width don't | |||
|
712 | # overflow the subject | |||
|
713 | if (i_left + i_width) >= 0 | |||
|
714 | options[:pdf].SetX(options[:subject_width] + i_left + i_width) | |||
|
715 | else | |||
|
716 | options[:pdf].SetX(options[:subject_width]) | |||
|
717 | end | |||
|
718 | options[:pdf].Cell(30, 2, "#{issue.status} #{issue.done_ratio}%") | |||
|
719 | end | |||
|
720 | else | |||
|
721 | ActiveRecord::Base.logger.debug "GanttHelper#line_for_issue was not given an issue with a due_before" | |||
|
722 | '' | |||
|
723 | end | |||
|
724 | end | |||
|
725 | ||||
92 | # Generates a gantt image |
|
726 | # Generates a gantt image | |
93 | # Only defined if RMagick is avalaible |
|
727 | # Only defined if RMagick is avalaible | |
94 |
def to_image( |
|
728 | def to_image(format='PNG') | |
95 | date_to = (@date_from >> @months)-1 |
|
729 | date_to = (@date_from >> @months)-1 | |
96 | show_weeks = @zoom > 1 |
|
730 | show_weeks = @zoom > 1 | |
97 | show_days = @zoom > 2 |
|
731 | show_days = @zoom > 2 | |
@@ -101,7 +735,7 module Redmine | |||||
101 | # width of one day in pixels |
|
735 | # width of one day in pixels | |
102 | zoom = @zoom*2 |
|
736 | zoom = @zoom*2 | |
103 | g_width = (@date_to - @date_from + 1)*zoom |
|
737 | g_width = (@date_to - @date_from + 1)*zoom | |
104 |
g_height = 20 * |
|
738 | g_height = 20 * number_of_rows + 30 | |
105 | headers_heigth = (show_weeks ? 2*header_heigth : header_heigth) |
|
739 | headers_heigth = (show_weeks ? 2*header_heigth : header_heigth) | |
106 | height = g_height + headers_heigth |
|
740 | height = g_height + headers_heigth | |
107 |
|
741 | |||
@@ -110,21 +744,7 module Redmine | |||||
110 | gc = Magick::Draw.new |
|
744 | gc = Magick::Draw.new | |
111 |
|
745 | |||
112 | # Subjects |
|
746 | # Subjects | |
113 | top = headers_heigth + 20 |
|
747 | subjects(:image => gc, :top => (headers_heigth + 20), :indent => 4, :format => :image) | |
114 | gc.fill('black') |
|
|||
115 | gc.stroke('transparent') |
|
|||
116 | gc.stroke_width(1) |
|
|||
117 | events.each do |i| |
|
|||
118 | text = "" |
|
|||
119 | if i.is_a? Issue |
|
|||
120 | text = "#{i.tracker} #{i.id}: #{i.subject}" |
|
|||
121 | else |
|
|||
122 | text = i.name |
|
|||
123 | end |
|
|||
124 | text = "#{i.project} - #{text}" unless project && project == i.project |
|
|||
125 | gc.text(4, top + 2, text) |
|
|||
126 | top = top + 20 |
|
|||
127 | end |
|
|||
128 |
|
748 | |||
129 | # Months headers |
|
749 | # Months headers | |
130 | month_f = @date_from |
|
750 | month_f = @date_from | |
@@ -202,38 +822,8 module Redmine | |||||
202 |
|
822 | |||
203 | # content |
|
823 | # content | |
204 | top = headers_heigth + 20 |
|
824 | top = headers_heigth + 20 | |
205 | gc.stroke('transparent') |
|
825 | ||
206 | events.each do |i| |
|
826 | lines(:image => gc, :top => top, :zoom => zoom, :subject_width => subject_width, :format => :image) | |
207 | if i.is_a?(Issue) |
|
|||
208 | i_start_date = (i.start_date >= @date_from ? i.start_date : @date_from ) |
|
|||
209 | i_end_date = (i.due_before <= date_to ? i.due_before : date_to ) |
|
|||
210 | i_done_date = i.start_date + ((i.due_before - i.start_date+1)*i.done_ratio/100).floor |
|
|||
211 | i_done_date = (i_done_date <= @date_from ? @date_from : i_done_date ) |
|
|||
212 | i_done_date = (i_done_date >= date_to ? date_to : i_done_date ) |
|
|||
213 | i_late_date = [i_end_date, Date.today].min if i_start_date < Date.today |
|
|||
214 |
|
||||
215 | i_left = subject_width + ((i_start_date - @date_from)*zoom).floor |
|
|||
216 | i_width = ((i_end_date - i_start_date + 1)*zoom).floor # total width of the issue |
|
|||
217 | d_width = ((i_done_date - i_start_date)*zoom).floor # done width |
|
|||
218 | l_width = i_late_date ? ((i_late_date - i_start_date+1)*zoom).floor : 0 # delay width |
|
|||
219 |
|
||||
220 | gc.fill('grey') |
|
|||
221 | gc.rectangle(i_left, top, i_left + i_width, top - 6) |
|
|||
222 | gc.fill('red') |
|
|||
223 | gc.rectangle(i_left, top, i_left + l_width, top - 6) if l_width > 0 |
|
|||
224 | gc.fill('blue') |
|
|||
225 | gc.rectangle(i_left, top, i_left + d_width, top - 6) if d_width > 0 |
|
|||
226 | gc.fill('black') |
|
|||
227 | gc.text(i_left + i_width + 5,top + 1, "#{i.status.name} #{i.done_ratio}%") |
|
|||
228 | else |
|
|||
229 | i_left = subject_width + ((i.start_date - @date_from)*zoom).floor |
|
|||
230 | gc.fill('green') |
|
|||
231 | gc.rectangle(i_left, top, i_left + 6, top - 6) |
|
|||
232 | gc.fill('black') |
|
|||
233 | gc.text(i_left + 11, top + 1, i.name) |
|
|||
234 | end |
|
|||
235 | top = top + 20 |
|
|||
236 | end |
|
|||
237 |
|
827 | |||
238 | # today red line |
|
828 | # today red line | |
239 | if Date.today >= @date_from and Date.today <= date_to |
|
829 | if Date.today >= @date_from and Date.today <= date_to | |
@@ -246,36 +836,137 module Redmine | |||||
246 | imgl.format = format |
|
836 | imgl.format = format | |
247 | imgl.to_blob |
|
837 | imgl.to_blob | |
248 | end if Object.const_defined?(:Magick) |
|
838 | end if Object.const_defined?(:Magick) | |
249 |
|
839 | |||
250 | private |
|
840 | def to_pdf | |
251 |
|
841 | pdf = ::Redmine::Export::PDF::IFPDF.new(current_language) | ||
252 | def gantt_issue_compare(x, y, issues) |
|
842 | pdf.SetTitle("#{l(:label_gantt)} #{project}") | |
253 | if x.parent_id == y.parent_id |
|
843 | pdf.AliasNbPages | |
254 | gantt_start_compare(x, y) |
|
844 | pdf.footer_date = format_date(Date.today) | |
255 | elsif x.is_ancestor_of?(y) |
|
845 | pdf.AddPage("L") | |
256 | -1 |
|
846 | pdf.SetFontStyle('B',12) | |
257 | elsif y.is_ancestor_of?(x) |
|
847 | pdf.SetX(15) | |
258 | 1 |
|
848 | pdf.Cell(PDF::LeftPaneWidth, 20, project.to_s) | |
259 |
|
|
849 | pdf.Ln | |
260 | ax = issues.select {|i| i.is_a?(Issue) && i.is_ancestor_of?(x) && !i.is_ancestor_of?(y) }.sort_by(&:lft).first |
|
850 | pdf.SetFontStyle('B',9) | |
261 | ay = issues.select {|i| i.is_a?(Issue) && i.is_ancestor_of?(y) && !i.is_ancestor_of?(x) }.sort_by(&:lft).first |
|
851 | ||
262 | if ax.nil? && ay.nil? |
|
852 | subject_width = PDF::LeftPaneWidth | |
263 | gantt_start_compare(x, y) |
|
853 | header_heigth = 5 | |
|
854 | ||||
|
855 | headers_heigth = header_heigth | |||
|
856 | show_weeks = false | |||
|
857 | show_days = false | |||
|
858 | ||||
|
859 | if self.months < 7 | |||
|
860 | show_weeks = true | |||
|
861 | headers_heigth = 2*header_heigth | |||
|
862 | if self.months < 3 | |||
|
863 | show_days = true | |||
|
864 | headers_heigth = 3*header_heigth | |||
|
865 | end | |||
|
866 | end | |||
|
867 | ||||
|
868 | g_width = PDF.right_pane_width | |||
|
869 | zoom = (g_width) / (self.date_to - self.date_from + 1) | |||
|
870 | g_height = 120 | |||
|
871 | t_height = g_height + headers_heigth | |||
|
872 | ||||
|
873 | y_start = pdf.GetY | |||
|
874 | ||||
|
875 | # Months headers | |||
|
876 | month_f = self.date_from | |||
|
877 | left = subject_width | |||
|
878 | height = header_heigth | |||
|
879 | self.months.times do | |||
|
880 | width = ((month_f >> 1) - month_f) * zoom | |||
|
881 | pdf.SetY(y_start) | |||
|
882 | pdf.SetX(left) | |||
|
883 | pdf.Cell(width, height, "#{month_f.year}-#{month_f.month}", "LTR", 0, "C") | |||
|
884 | left = left + width | |||
|
885 | month_f = month_f >> 1 | |||
|
886 | end | |||
|
887 | ||||
|
888 | # Weeks headers | |||
|
889 | if show_weeks | |||
|
890 | left = subject_width | |||
|
891 | height = header_heigth | |||
|
892 | if self.date_from.cwday == 1 | |||
|
893 | # self.date_from is monday | |||
|
894 | week_f = self.date_from | |||
264 | else |
|
895 | else | |
265 | gantt_issue_compare(ax || x, ay || y, issues) |
|
896 | # find next monday after self.date_from | |
|
897 | week_f = self.date_from + (7 - self.date_from.cwday + 1) | |||
|
898 | width = (7 - self.date_from.cwday + 1) * zoom-1 | |||
|
899 | pdf.SetY(y_start + header_heigth) | |||
|
900 | pdf.SetX(left) | |||
|
901 | pdf.Cell(width + 1, height, "", "LTR") | |||
|
902 | left = left + width+1 | |||
|
903 | end | |||
|
904 | while week_f <= self.date_to | |||
|
905 | width = (week_f + 6 <= self.date_to) ? 7 * zoom : (self.date_to - week_f + 1) * zoom | |||
|
906 | pdf.SetY(y_start + header_heigth) | |||
|
907 | pdf.SetX(left) | |||
|
908 | pdf.Cell(width, height, (width >= 5 ? week_f.cweek.to_s : ""), "LTR", 0, "C") | |||
|
909 | left = left + width | |||
|
910 | week_f = week_f+7 | |||
|
911 | end | |||
|
912 | end | |||
|
913 | ||||
|
914 | # Days headers | |||
|
915 | if show_days | |||
|
916 | left = subject_width | |||
|
917 | height = header_heigth | |||
|
918 | wday = self.date_from.cwday | |||
|
919 | pdf.SetFontStyle('B',7) | |||
|
920 | (self.date_to - self.date_from + 1).to_i.times do | |||
|
921 | width = zoom | |||
|
922 | pdf.SetY(y_start + 2 * header_heigth) | |||
|
923 | pdf.SetX(left) | |||
|
924 | pdf.Cell(width, height, day_name(wday).first, "LTR", 0, "C") | |||
|
925 | left = left + width | |||
|
926 | wday = wday + 1 | |||
|
927 | wday = 1 if wday > 7 | |||
266 | end |
|
928 | end | |
267 | end |
|
929 | end | |
|
930 | ||||
|
931 | pdf.SetY(y_start) | |||
|
932 | pdf.SetX(15) | |||
|
933 | pdf.Cell(subject_width+g_width-15, headers_heigth, "", 1) | |||
|
934 | ||||
|
935 | # Tasks | |||
|
936 | top = headers_heigth + y_start | |||
|
937 | pdf_subjects_and_lines(pdf, { | |||
|
938 | :top => top, | |||
|
939 | :zoom => zoom, | |||
|
940 | :subject_width => subject_width, | |||
|
941 | :g_width => g_width | |||
|
942 | }) | |||
|
943 | ||||
|
944 | ||||
|
945 | pdf.Line(15, top, subject_width+g_width, top) | |||
|
946 | pdf.Output | |||
|
947 | ||||
|
948 | ||||
268 | end |
|
949 | end | |
269 |
|
950 | |||
270 | def gantt_start_compare(x, y) |
|
951 | private | |
271 | if x.start_date.nil? |
|
952 | ||
272 | -1 |
|
953 | # Renders both the subjects and lines of the Gantt chart for the | |
273 | elsif y.start_date.nil? |
|
954 | # PDF format | |
274 | 1 |
|
955 | 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) | |||
|
957 | line_options = {:indent => 0, :indent_increment => 5, :top_increment => 3, :render => :line, :format => :pdf, :pdf => pdf}.merge(options) | |||
|
958 | ||||
|
959 | if @project | |||
|
960 | render_project(@project, subject_options) | |||
|
961 | render_project(@project, line_options) | |||
275 | else |
|
962 | else | |
276 | x.start_date <=> y.start_date |
|
963 | Project.roots.each do |project| | |
|
964 | render_project(project, subject_options) | |||
|
965 | render_project(project, line_options) | |||
|
966 | end | |||
277 | end |
|
967 | end | |
278 | end |
|
968 | end | |
|
969 | ||||
279 | end |
|
970 | end | |
280 | end |
|
971 | end | |
281 | end |
|
972 | end |
1 | NO CONTENT: modified file, binary diff hidden |
|
NO CONTENT: modified file, binary diff hidden |
@@ -787,8 +787,10 background-image:url('../images/close_hl.png'); | |||||
787 | white-space:nowrap; |
|
787 | white-space:nowrap; | |
788 | } |
|
788 | } | |
789 |
|
789 | |||
|
790 | .task.label {width:100%;} | |||
|
791 | ||||
790 | .task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; } |
|
792 | .task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; } | |
791 |
.task_done { background:# |
|
793 | .task_done { background:#00c600 url(../images/task_done.png); border: 1px solid #00c600; } | |
792 | .task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; } |
|
794 | .task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; } | |
793 |
|
795 | |||
794 | .task_todo.parent { background: #888; border: 1px solid #888; height: 6px;} |
|
796 | .task_todo.parent { background: #888; border: 1px solid #888; height: 6px;} | |
@@ -796,7 +798,17 background-image:url('../images/close_hl.png'); | |||||
796 | .task_todo.parent .left { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-left: -5px; left: 0px; top: -1px;} |
|
798 | .task_todo.parent .left { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-left: -5px; left: 0px; top: -1px;} | |
797 | .task_todo.parent .right { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-right: -5px; right: 0px; top: -1px;} |
|
799 | .task_todo.parent .right { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-right: -5px; right: 0px; top: -1px;} | |
798 |
|
800 | |||
799 |
.milestone { background-image:url(../images/ |
|
801 | .milestone { background-image:url(../images/version_marker.png); background-repeat: no-repeat; border: 0; } | |
|
802 | .milestone_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;} | |||
|
803 | .milestone_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;} | |||
|
804 | .milestone_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;} | |||
|
805 | .project-line { background-image:url(../images/project_marker.png); background-repeat: no-repeat; border: 0; } | |||
|
806 | .project_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;} | |||
|
807 | .project_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;} | |||
|
808 | .project_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;} | |||
|
809 | ||||
|
810 | .version-behind-schedule a, .issue-behind-schedule a {color: #f66914;} | |||
|
811 | .version-overdue a, .issue-overdue a, .project-overdue a {color: #f00;} | |||
800 |
|
812 | |||
801 | /***** Icons *****/ |
|
813 | /***** Icons *****/ | |
802 | .icon { |
|
814 | .icon { | |
@@ -840,6 +852,7 padding-bottom: 3px; | |||||
840 | .icon-comment { background-image: url(../images/comment.png); } |
|
852 | .icon-comment { background-image: url(../images/comment.png); } | |
841 | .icon-summary { background-image: url(../images/lightning.png); } |
|
853 | .icon-summary { background-image: url(../images/lightning.png); } | |
842 | .icon-server-authentication { background-image: url(../images/server_key.png); } |
|
854 | .icon-server-authentication { background-image: url(../images/server_key.png); } | |
|
855 | .icon-issue { background-image: url(../images/ticket.png); } | |||
843 |
|
856 | |||
844 | .icon-file { background-image: url(../images/files/default.png); } |
|
857 | .icon-file { background-image: url(../images/files/default.png); } | |
845 | .icon-file.text-plain { background-image: url(../images/files/text.png); } |
|
858 | .icon-file.text-plain { background-image: url(../images/files/text.png); } | |
@@ -898,6 +911,12 td.username img.gravatar { | |||||
898 | margin: 0 1em 1em 0; |
|
911 | margin: 0 1em 1em 0; | |
899 | } |
|
912 | } | |
900 |
|
913 | |||
|
914 | /* Used on 12px Gravatar img tags without the icon background */ | |||
|
915 | .icon-gravatar { | |||
|
916 | float: left; | |||
|
917 | margin-right: 4px; | |||
|
918 | } | |||
|
919 | ||||
901 | #activity dt, |
|
920 | #activity dt, | |
902 | .journal { |
|
921 | .journal { | |
903 | clear: left; |
|
922 | clear: left; |
@@ -5,20 +5,20 class GanttsControllerTest < ActionController::TestCase | |||||
5 |
|
5 | |||
6 | context "#gantt" do |
|
6 | context "#gantt" do | |
7 | should "work" do |
|
7 | should "work" do | |
|
8 | i2 = Issue.find(2) | |||
|
9 | i2.update_attribute(:due_date, 1.month.from_now) | |||
|
10 | ||||
8 | get :show, :project_id => 1 |
|
11 | get :show, :project_id => 1 | |
9 | assert_response :success |
|
12 | assert_response :success | |
10 | assert_template 'show.html.erb' |
|
13 | assert_template 'show.html.erb' | |
11 | assert_not_nil assigns(:gantt) |
|
14 | assert_not_nil assigns(:gantt) | |
12 | events = assigns(:gantt).events |
|
|||
13 | assert_not_nil events |
|
|||
14 | # Issue with start and due dates |
|
15 | # Issue with start and due dates | |
15 | i = Issue.find(1) |
|
16 | i = Issue.find(1) | |
16 | assert_not_nil i.due_date |
|
17 | assert_not_nil i.due_date | |
17 | assert events.include?(Issue.find(1)) |
|
18 | assert_select "div a.issue", /##{i.id}/ | |
18 | # Issue with without due date but targeted to a version with date |
|
19 | # Issue with on a targeted version should not be in the events but loaded in the html | |
19 | i = Issue.find(2) |
|
20 | i = Issue.find(2) | |
20 | assert_nil i.due_date |
|
21 | assert_select "div a.issue", /##{i.id}/ | |
21 | assert events.include?(i) |
|
|||
22 | end |
|
22 | end | |
23 |
|
23 | |||
24 | should "work cross project" do |
|
24 | should "work cross project" do | |
@@ -26,8 +26,8 class GanttsControllerTest < ActionController::TestCase | |||||
26 | assert_response :success |
|
26 | assert_response :success | |
27 | assert_template 'show.html.erb' |
|
27 | assert_template 'show.html.erb' | |
28 | assert_not_nil assigns(:gantt) |
|
28 | assert_not_nil assigns(:gantt) | |
29 |
|
|
29 | assert_not_nil assigns(:gantt).query | |
30 | assert_not_nil events |
|
30 | assert_nil assigns(:gantt).project | |
31 | end |
|
31 | end | |
32 |
|
32 | |||
33 | should "export to pdf" do |
|
33 | should "export to pdf" do |
@@ -25,8 +25,9 module ObjectDaddyHelpers | |||||
25 | def Issue.generate_for_project!(project, attributes={}) |
|
25 | def Issue.generate_for_project!(project, attributes={}) | |
26 | issue = Issue.spawn(attributes) do |issue| |
|
26 | issue = Issue.spawn(attributes) do |issue| | |
27 | issue.project = project |
|
27 | issue.project = project | |
|
28 | issue.tracker = project.trackers.first unless project.trackers.empty? | |||
|
29 | yield issue if block_given? | |||
28 | end |
|
30 | end | |
29 | issue.tracker = project.trackers.first unless project.trackers.empty? |
|
|||
30 | issue.save! |
|
31 | issue.save! | |
31 | issue |
|
32 | issue | |
32 | end |
|
33 | end |
@@ -601,7 +601,7 EXPECTED | |||||
601 |
|
601 | |||
602 | # turn off avatars |
|
602 | # turn off avatars | |
603 | Setting.gravatar_enabled = '0' |
|
603 | Setting.gravatar_enabled = '0' | |
604 |
assert_ |
|
604 | assert_equal '', avatar(User.find_by_mail('jsmith@somenet.foo')) | |
605 | end |
|
605 | end | |
606 |
|
606 | |||
607 | def test_link_to_user |
|
607 | def test_link_to_user |
@@ -510,6 +510,28 class IssueTest < ActiveSupport::TestCase | |||||
510 | assert !Issue.new(:due_date => nil).overdue? |
|
510 | assert !Issue.new(:due_date => nil).overdue? | |
511 | assert !Issue.new(:due_date => 1.day.ago.to_date, :status => IssueStatus.find(:first, :conditions => {:is_closed => true})).overdue? |
|
511 | assert !Issue.new(:due_date => 1.day.ago.to_date, :status => IssueStatus.find(:first, :conditions => {:is_closed => true})).overdue? | |
512 | end |
|
512 | end | |
|
513 | ||||
|
514 | context "#behind_schedule?" do | |||
|
515 | should "be false if the issue has no start_date" do | |||
|
516 | assert !Issue.new(:start_date => nil, :due_date => 1.day.from_now.to_date, :done_ratio => 0).behind_schedule? | |||
|
517 | end | |||
|
518 | ||||
|
519 | should "be false if the issue has no end_date" do | |||
|
520 | assert !Issue.new(:start_date => 1.day.from_now.to_date, :due_date => nil, :done_ratio => 0).behind_schedule? | |||
|
521 | end | |||
|
522 | ||||
|
523 | should "be false if the issue has more done than it's calendar time" do | |||
|
524 | assert !Issue.new(:start_date => 50.days.ago.to_date, :due_date => 50.days.from_now.to_date, :done_ratio => 90).behind_schedule? | |||
|
525 | end | |||
|
526 | ||||
|
527 | should "be true if the issue hasn't been started at all" do | |||
|
528 | assert Issue.new(:start_date => 1.day.ago.to_date, :due_date => 1.day.from_now.to_date, :done_ratio => 0).behind_schedule? | |||
|
529 | end | |||
|
530 | ||||
|
531 | should "be true if the issue has used more calendar time than it's done ratio" do | |||
|
532 | assert Issue.new(:start_date => 100.days.ago.to_date, :due_date => Date.today, :done_ratio => 90).behind_schedule? | |||
|
533 | end | |||
|
534 | end | |||
513 |
|
535 | |||
514 | def test_assignable_users |
|
536 | def test_assignable_users | |
515 | assert_kind_of User, Issue.find(1).assignable_users.first |
|
537 | assert_kind_of User, Issue.find(1).assignable_users.first |
@@ -842,4 +842,122 class ProjectTest < ActiveSupport::TestCase | |||||
842 |
|
842 | |||
843 | end |
|
843 | end | |
844 |
|
844 | |||
|
845 | context "#start_date" do | |||
|
846 | setup do | |||
|
847 | ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests | |||
|
848 | @project = Project.generate!(:identifier => 'test0') | |||
|
849 | @project.trackers << Tracker.generate! | |||
|
850 | end | |||
|
851 | ||||
|
852 | should "be nil if there are no issues on the project" do | |||
|
853 | assert_nil @project.start_date | |||
|
854 | end | |||
|
855 | ||||
|
856 | should "be nil if issue tracking is disabled" do | |||
|
857 | Issue.generate_for_project!(@project, :start_date => Date.today) | |||
|
858 | @project.enabled_modules.find_all_by_name('issue_tracking').each {|m| m.destroy} | |||
|
859 | @project.reload | |||
|
860 | ||||
|
861 | assert_nil @project.start_date | |||
|
862 | end | |||
|
863 | ||||
|
864 | should "be the earliest start date of it's issues" do | |||
|
865 | early = 7.days.ago.to_date | |||
|
866 | Issue.generate_for_project!(@project, :start_date => Date.today) | |||
|
867 | Issue.generate_for_project!(@project, :start_date => early) | |||
|
868 | ||||
|
869 | assert_equal early, @project.start_date | |||
|
870 | end | |||
|
871 | ||||
|
872 | end | |||
|
873 | ||||
|
874 | context "#due_date" do | |||
|
875 | setup do | |||
|
876 | ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests | |||
|
877 | @project = Project.generate!(:identifier => 'test0') | |||
|
878 | @project.trackers << Tracker.generate! | |||
|
879 | end | |||
|
880 | ||||
|
881 | should "be nil if there are no issues on the project" do | |||
|
882 | assert_nil @project.due_date | |||
|
883 | end | |||
|
884 | ||||
|
885 | should "be nil if issue tracking is disabled" do | |||
|
886 | Issue.generate_for_project!(@project, :due_date => Date.today) | |||
|
887 | @project.enabled_modules.find_all_by_name('issue_tracking').each {|m| m.destroy} | |||
|
888 | @project.reload | |||
|
889 | ||||
|
890 | assert_nil @project.due_date | |||
|
891 | end | |||
|
892 | ||||
|
893 | should "be the latest due date of it's issues" do | |||
|
894 | future = 7.days.from_now.to_date | |||
|
895 | Issue.generate_for_project!(@project, :due_date => future) | |||
|
896 | Issue.generate_for_project!(@project, :due_date => Date.today) | |||
|
897 | ||||
|
898 | assert_equal future, @project.due_date | |||
|
899 | end | |||
|
900 | ||||
|
901 | should "be the latest due date of it's versions" do | |||
|
902 | future = 7.days.from_now.to_date | |||
|
903 | @project.versions << Version.generate!(:effective_date => future) | |||
|
904 | @project.versions << Version.generate!(:effective_date => Date.today) | |||
|
905 | ||||
|
906 | ||||
|
907 | assert_equal future, @project.due_date | |||
|
908 | ||||
|
909 | end | |||
|
910 | ||||
|
911 | should "pick the latest date from it's issues and versions" do | |||
|
912 | future = 7.days.from_now.to_date | |||
|
913 | far_future = 14.days.from_now.to_date | |||
|
914 | Issue.generate_for_project!(@project, :due_date => far_future) | |||
|
915 | @project.versions << Version.generate!(:effective_date => future) | |||
|
916 | ||||
|
917 | assert_equal far_future, @project.due_date | |||
|
918 | end | |||
|
919 | ||||
|
920 | end | |||
|
921 | ||||
|
922 | context "Project#completed_percent" do | |||
|
923 | setup do | |||
|
924 | ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests | |||
|
925 | @project = Project.generate!(:identifier => 'test0') | |||
|
926 | @project.trackers << Tracker.generate! | |||
|
927 | end | |||
|
928 | ||||
|
929 | context "no versions" do | |||
|
930 | should "be 100" do | |||
|
931 | assert_equal 100, @project.completed_percent | |||
|
932 | end | |||
|
933 | end | |||
|
934 | ||||
|
935 | context "with versions" do | |||
|
936 | should "return 0 if the versions have no issues" do | |||
|
937 | Version.generate!(:project => @project) | |||
|
938 | Version.generate!(:project => @project) | |||
|
939 | ||||
|
940 | assert_equal 0, @project.completed_percent | |||
|
941 | end | |||
|
942 | ||||
|
943 | should "return 100 if the version has only closed issues" do | |||
|
944 | v1 = Version.generate!(:project => @project) | |||
|
945 | Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('Closed'), :fixed_version => v1) | |||
|
946 | v2 = Version.generate!(:project => @project) | |||
|
947 | Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('Closed'), :fixed_version => v2) | |||
|
948 | ||||
|
949 | assert_equal 100, @project.completed_percent | |||
|
950 | end | |||
|
951 | ||||
|
952 | should "return the averaged completed percent of the versions (not weighted)" do | |||
|
953 | v1 = Version.generate!(:project => @project) | |||
|
954 | Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('New'), :estimated_hours => 10, :done_ratio => 50, :fixed_version => v1) | |||
|
955 | v2 = Version.generate!(:project => @project) | |||
|
956 | Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('New'), :estimated_hours => 10, :done_ratio => 50, :fixed_version => v2) | |||
|
957 | ||||
|
958 | assert_equal 50, @project.completed_percent | |||
|
959 | end | |||
|
960 | ||||
|
961 | end | |||
|
962 | end | |||
845 | end |
|
963 | end |
@@ -104,7 +104,57 class VersionTest < ActiveSupport::TestCase | |||||
104 | assert_progress_equal (25.0*0.2 + 25.0*1 + 10.0*0.3 + 40.0*0.1)/100.0*100, v.completed_pourcent |
|
104 | assert_progress_equal (25.0*0.2 + 25.0*1 + 10.0*0.3 + 40.0*0.1)/100.0*100, v.completed_pourcent | |
105 | assert_progress_equal 25.0/100.0*100, v.closed_pourcent |
|
105 | assert_progress_equal 25.0/100.0*100, v.closed_pourcent | |
106 | end |
|
106 | end | |
107 |
|
107 | |||
|
108 | context "#behind_schedule?" do | |||
|
109 | setup do | |||
|
110 | ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests | |||
|
111 | @project = Project.generate!(:identifier => 'test0') | |||
|
112 | @project.trackers << Tracker.generate! | |||
|
113 | ||||
|
114 | @version = Version.generate!(:project => @project, :effective_date => nil) | |||
|
115 | end | |||
|
116 | ||||
|
117 | should "be false if there are no issues assigned" do | |||
|
118 | @version.update_attribute(:effective_date, Date.yesterday) | |||
|
119 | assert_equal false, @version.behind_schedule? | |||
|
120 | end | |||
|
121 | ||||
|
122 | should "be false if there is no effective_date" do | |||
|
123 | assert_equal false, @version.behind_schedule? | |||
|
124 | end | |||
|
125 | ||||
|
126 | should "be false if all of the issues are ahead of schedule" do | |||
|
127 | @version.update_attribute(:effective_date, 7.days.from_now.to_date) | |||
|
128 | @version.fixed_issues = [ | |||
|
129 | Issue.generate_for_project!(@project, :start_date => 7.days.ago, :done_ratio => 60), # 14 day span, 60% done, 50% time left | |||
|
130 | Issue.generate_for_project!(@project, :start_date => 7.days.ago, :done_ratio => 60) # 14 day span, 60% done, 50% time left | |||
|
131 | ] | |||
|
132 | assert_equal 60, @version.completed_pourcent | |||
|
133 | assert_equal false, @version.behind_schedule? | |||
|
134 | end | |||
|
135 | ||||
|
136 | should "be true if any of the issues are behind schedule" do | |||
|
137 | @version.update_attribute(:effective_date, 7.days.from_now.to_date) | |||
|
138 | @version.fixed_issues = [ | |||
|
139 | Issue.generate_for_project!(@project, :start_date => 7.days.ago, :done_ratio => 60), # 14 day span, 60% done, 50% time left | |||
|
140 | Issue.generate_for_project!(@project, :start_date => 7.days.ago, :done_ratio => 20) # 14 day span, 20% done, 50% time left | |||
|
141 | ] | |||
|
142 | assert_equal 40, @version.completed_pourcent | |||
|
143 | assert_equal true, @version.behind_schedule? | |||
|
144 | end | |||
|
145 | ||||
|
146 | should "be false if all of the issues are complete" do | |||
|
147 | @version.update_attribute(:effective_date, 7.days.from_now.to_date) | |||
|
148 | @version.fixed_issues = [ | |||
|
149 | Issue.generate_for_project!(@project, :start_date => 14.days.ago, :done_ratio => 100, :status => IssueStatus.find(5)), # 7 day span | |||
|
150 | Issue.generate_for_project!(@project, :start_date => 14.days.ago, :done_ratio => 100, :status => IssueStatus.find(5)) # 7 day span | |||
|
151 | ] | |||
|
152 | assert_equal 100, @version.completed_pourcent | |||
|
153 | assert_equal false, @version.behind_schedule? | |||
|
154 | ||||
|
155 | end | |||
|
156 | end | |||
|
157 | ||||
108 | context "#estimated_hours" do |
|
158 | context "#estimated_hours" do | |
109 | setup do |
|
159 | setup do | |
110 | @version = Version.create!(:project_id => 1, :name => '#estimated_hours') |
|
160 | @version = Version.create!(:project_id => 1, :name => '#estimated_hours') |
@@ -6,7 +6,7 task :default => :spec | |||||
6 |
|
6 | |||
7 | desc 'Run all application-specific specs' |
|
7 | desc 'Run all application-specific specs' | |
8 | Spec::Rake::SpecTask.new(:spec) do |t| |
|
8 | Spec::Rake::SpecTask.new(:spec) do |t| | |
9 | t.rcov = true |
|
9 | # t.rcov = true | |
10 | end |
|
10 | end | |
11 |
|
11 | |||
12 | desc "Report code statistics (KLOCs, etc) from the application" |
|
12 | desc "Report code statistics (KLOCs, etc) from the application" |
@@ -26,6 +26,9 module GravatarHelper | |||||
26 | # decorational picture, the alt text should be empty according to the |
|
26 | # decorational picture, the alt text should be empty according to the | |
27 | # XHTML specs. |
|
27 | # XHTML specs. | |
28 | :alt => '', |
|
28 | :alt => '', | |
|
29 | ||||
|
30 | # The title text to use for the img tag for the gravatar. | |||
|
31 | :title => '', | |||
29 |
|
32 | |||
30 | # The class to assign to the img tag for the gravatar. |
|
33 | # The class to assign to the img tag for the gravatar. | |
31 | :class => 'gravatar', |
|
34 | :class => 'gravatar', | |
@@ -48,8 +51,8 module GravatarHelper | |||||
48 | def gravatar(email, options={}) |
|
51 | def gravatar(email, options={}) | |
49 | src = h(gravatar_url(email, options)) |
|
52 | src = h(gravatar_url(email, options)) | |
50 | options = DEFAULT_OPTIONS.merge(options) |
|
53 | options = DEFAULT_OPTIONS.merge(options) | |
51 | [:class, :alt, :size].each { |opt| options[opt] = h(options[opt]) } |
|
54 | [:class, :alt, :size, :title].each { |opt| options[opt] = h(options[opt]) } | |
52 |
"<img class=\"#{options[:class]}\" alt=\"#{options[:alt]}\" width=\"#{options[:size]}\" height=\"#{options[:size]}\" src=\"#{src}\" />" |
|
55 | "<img class=\"#{options[:class]}\" alt=\"#{options[:alt]}\" title=\"#{options[:title]}\" width=\"#{options[:size]}\" height=\"#{options[:size]}\" src=\"#{src}\" />" | |
53 | end |
|
56 | end | |
54 |
|
57 | |||
55 | # Returns the base Gravatar URL for the given email hash. If ssl evaluates to true, |
|
58 | # Returns the base Gravatar URL for the given email hash. If ssl evaluates to true, | |
@@ -82,4 +85,4 module GravatarHelper | |||||
82 |
|
85 | |||
83 | end |
|
86 | end | |
84 |
|
87 | |||
85 | end No newline at end of file |
|
88 | end |
@@ -4,34 +4,40 require 'active_support' # to get "returning" | |||||
4 | require File.dirname(__FILE__) + '/../lib/gravatar' |
|
4 | require File.dirname(__FILE__) + '/../lib/gravatar' | |
5 | include GravatarHelper, GravatarHelper::PublicMethods, ERB::Util |
|
5 | include GravatarHelper, GravatarHelper::PublicMethods, ERB::Util | |
6 |
|
6 | |||
7 |
|
|
7 | describe "gravatar_url with a custom default URL" do | |
8 | setup do |
|
8 | before(:each) do | |
9 | @original_options = DEFAULT_OPTIONS.dup |
|
9 | @original_options = DEFAULT_OPTIONS.dup | |
10 | DEFAULT_OPTIONS[:default] = "no_avatar.png" |
|
10 | DEFAULT_OPTIONS[:default] = "no_avatar.png" | |
11 | @url = gravatar_url("somewhere") |
|
11 | @url = gravatar_url("somewhere") | |
12 | end |
|
12 | end | |
13 |
|
13 | |||
14 |
|
|
14 | it "should include the \"default\" argument in the result" do | |
15 | @url.should match(/&default=no_avatar.png/) |
|
15 | @url.should match(/&default=no_avatar.png/) | |
16 | end |
|
16 | end | |
17 |
|
17 | |||
18 | teardown do |
|
18 | after(:each) do | |
19 | DEFAULT_OPTIONS.merge!(@original_options) |
|
19 | DEFAULT_OPTIONS.merge!(@original_options) | |
20 | end |
|
20 | end | |
21 |
|
21 | |||
22 | end |
|
22 | end | |
23 |
|
23 | |||
24 |
|
|
24 | describe "gravatar_url with default settings" do | |
25 | setup do |
|
25 | before(:each) do | |
26 | @url = gravatar_url("somewhere") |
|
26 | @url = gravatar_url("somewhere") | |
27 | end |
|
27 | end | |
28 |
|
28 | |||
29 |
|
|
29 | it "should have a nil default URL" do | |
30 | DEFAULT_OPTIONS[:default].should be_nil |
|
30 | DEFAULT_OPTIONS[:default].should be_nil | |
31 | end |
|
31 | end | |
32 |
|
32 | |||
33 |
|
|
33 | it "should not include the \"default\" argument in the result" do | |
34 | @url.should_not match(/&default=/) |
|
34 | @url.should_not match(/&default=/) | |
35 | end |
|
35 | end | |
36 |
|
36 | |||
37 | end No newline at end of file |
|
37 | end | |
|
38 | ||||
|
39 | describe "gravatar with a custom title option" do | |||
|
40 | it "should include the title in the result" do | |||
|
41 | gravatar('example@example.com', :title => "This is a title attribute").should match(/This is a title attribute/) | |||
|
42 | end | |||
|
43 | end |
1 | NO CONTENT: file was removed, binary diff hidden |
|
NO CONTENT: file was removed, binary diff hidden |
General Comments 0
You need to be logged in to leave comments.
Login now