##// END OF EJS Templates
Rewrite the Gantt chart. #6276...
Eric Davis -
r3958:bdb3937e0f4c
parent child
Show More
@@ -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(@project), :disposition => 'inline', :type => 'image/png', :filename => "#{basename}.png") } if @gantt.respond_to?('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(gantt_to_pdf(@gantt, @project), :type => 'application/pdf', :filename => "#{basename}.pdf") }
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 => "#{self.table_name}.updated_on DESC"
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>&nbsp;<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">&nbsp;</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">&nbsp;</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">&nbsp;</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'>&nbsp;</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'>&nbsp;</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'>&nbsp;</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'>&nbsp;</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'>&nbsp;</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'>&nbsp;</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'>&nbsp;</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'>&nbsp;</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'>&nbsp;</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'>&nbsp;</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'>&nbsp;</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'>&nbsp;</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'>&nbsp;</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(project, format='PNG')
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 * events.length + 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 else
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:#66f url(../images/task_done.png); border: 1px solid #66f; }
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/milestone.png); background-repeat: no-repeat; border: 0; }
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 events = assigns(:gantt).events
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_nil avatar(User.find_by_mail('jsmith@somenet.foo'))
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 context "gravatar_url with a custom default URL" do
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 specify "should include the \"default\" argument in the result" do
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 context "gravatar_url with default settings" do
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 specify "should have a nil default URL" do
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 specify "should not include the \"default\" argument in the result" do
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