├── .rspec ├── spec ├── rcov.opts ├── ci_config │ └── database.ci.yml ├── spec.opts ├── sanity_spec.rb ├── ci.sh ├── controllers │ ├── issues_controller_spec.rb │ └── gantts_controller_spec.rb ├── helpers.rb ├── spec_helper.rb ├── models │ ├── project_patch_spec.rb │ └── issue_patch_spec.rb ├── factories.rb └── lib │ └── calendar_spec.rb ├── .rvmrc ├── lang └── en.yml ├── Gemfile.lock ├── screenshots ├── gantt_html_zoom1-year.png ├── gantt_html_zoom3-week.png ├── gantt_html_zoom4-day.png └── gantt_html_zoom2-month.png ├── ISSUE_TEMPLATE.md ├── app └── views │ ├── issues │ └── _show_estimated_duration.html.erb │ ├── settings │ └── _better_gantt_chart_settings.html.erb │ └── gantts │ └── show.html.erb ├── .travis.yml ├── lib ├── redmine_better_gantt_chart │ ├── redmine_better_gantt_chart.rb │ ├── hooks │ │ └── view_issues_show_details_bottom_hook.rb │ ├── issue_patch.rb │ ├── project_patch.rb │ ├── gantts_controller_patch.rb │ ├── patches.rb │ ├── active_record │ │ ├── callback_extensions_for_rails3.rb │ │ └── callback_extensions_for_rails2.rb │ ├── issues_helper_patch.rb │ ├── calendar.rb │ └── issue_dependency_patch.rb └── redmine │ └── helpers │ └── better_gantt.rb ├── assets └── javascripts │ ├── compile_coffee.bat │ ├── raphael.arrow.coffee │ ├── raphael.arrow.js │ └── raphael-min.js ├── Gemfile ├── Rakefile ├── config └── locales │ ├── en.yml │ ├── fr.yml │ ├── pt-BR.yml │ ├── de.yml │ └── bg.yml ├── init.rb └── README.rdoc /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | -------------------------------------------------------------------------------- /spec/rcov.opts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.rvmrc: -------------------------------------------------------------------------------- 1 | rvm use 1.9.3 2 | -------------------------------------------------------------------------------- /lang/en.yml: -------------------------------------------------------------------------------- 1 | # English strings go here 2 | my_label: "My label" 3 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | specs: 3 | 4 | PLATFORMS 5 | ruby 6 | 7 | DEPENDENCIES 8 | -------------------------------------------------------------------------------- /spec/ci_config/database.ci.yml: -------------------------------------------------------------------------------- 1 | test: 2 | adapter: sqlite3 3 | database: db/test.sqlite3 4 | -------------------------------------------------------------------------------- /spec/spec.opts: -------------------------------------------------------------------------------- 1 | --colour 2 | --format 3 | nested 4 | --loadby 5 | mtime 6 | --reverse 7 | --backtrace 8 | -------------------------------------------------------------------------------- /screenshots/gantt_html_zoom1-year.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kulesa/redmine_better_gantt_chart/HEAD/screenshots/gantt_html_zoom1-year.png -------------------------------------------------------------------------------- /screenshots/gantt_html_zoom3-week.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kulesa/redmine_better_gantt_chart/HEAD/screenshots/gantt_html_zoom3-week.png -------------------------------------------------------------------------------- /screenshots/gantt_html_zoom4-day.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kulesa/redmine_better_gantt_chart/HEAD/screenshots/gantt_html_zoom4-day.png -------------------------------------------------------------------------------- /screenshots/gantt_html_zoom2-month.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kulesa/redmine_better_gantt_chart/HEAD/screenshots/gantt_html_zoom2-month.png -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Hello! 2 | ### This is to let you know that this project is unmaintained. 3 | ### If you'd like to adopt this repo, please contact me! 4 | -------------------------------------------------------------------------------- /spec/sanity_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../spec_helper', __FILE__) 2 | 3 | describe Class do 4 | it "should be a class of Class" do 5 | Class.class.should eql(Class) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/views/issues/_show_estimated_duration.html.erb: -------------------------------------------------------------------------------- 1 |
2 | 3 | <%= check_box_tag('settings[work_on_weekends]', @settings['work_on_weekends'], @settings['work_on_weekends']) %> 4 |
5 |
6 | <%= h l(:label_enabled) %>: <%= h l(:description_enabled) %>
7 |
<%= h l(:label_disabled) %>: <%= h l(:description_disabled) %>
8 |
10 |
11 |
12 | <%= check_box_tag('settings[smart_sorting]', @settings['smart_sorting'], @settings['smart_sorting']) %>
13 |
}[http://travis-ci.org/kulesa/redmine_better_gantt_chart]
4 |
5 | The plugin improves functionality of Redmine Gantt Chart.
6 |
7 | == Features
8 |
9 | - Issues on Gantt chart connected with arrows. Handy!
10 | - Relations info added to issue tooltip.
11 | - *NEW*: smart sorting of issues on the chart. Now issues are sorted just like you'd expect them to.
12 | - *NEW*: rescheduling takes into account weekends, if this setting is enabled
13 | - Fixed rescheduling of related tasks if due date of dependent task changed to an earlier date.
14 | - Fixed sorting of issues on the chart as per http://www.redmine.org/issues/7335
15 | - Added validation: start date of child issue cannot be less than start date of the parent, if parent depends on other tasks
16 | - Fast rescheduling of related issues. Now you can have hundreds of related issues with many levels of hierarchy, and expect they'll be rescheduled just almost as fast as if you were in MS Project. And it will not cause 'stack level too deep' error (not kidding)!
17 |
18 | == Compatibility
19 |
20 | Tested with Redmine versions: 1.1.0, 1.1.1, 1.1.2, 1.1.3, 1.2, 1.3, 1.4, 2.2, 2.3, 2.4
21 |
22 | == Installation and Setup
23 |
24 | 1. Go to releases page: https://github.com/kulesa/redmine_better_gantt_chart/releases
25 | 2. For Redmine 2.x above download {version 0.9.0}[https://github.com/kulesa/redmine_better_gantt_chart/releases/download/v.0.9.0/redmine_better_gantt_chart_0.9.0.zip] or above. For Redmine 1.x download {version 0.6.1}[https://github.com/kulesa/redmine_better_gantt_chart/archive/v0.6.1.zip].
26 | 3. Go to your #{RAILS_ROOT}/vendor/plugins directory
27 | 4. Unzip downloaded archive and rename extracted folder to '*redmine_better_gantt_chart*' (if extracted folder has other name).
28 | 5. Restart Redmine
29 |
30 | You should now see the plugin in *Administration* -> *Plugins*. The plugin does not require any database migrations.
31 |
32 | Connection arrows are rendered in SVG via {Raphael.js}[http://raphaeljs.com/] library. This should work in any modern or not so modern browser, including IE6.
33 |
34 | == Usage
35 |
36 | === Relations
37 |
38 | 1. Open an existing issue in Redmine
39 | 2. Click *Add* in *Related* *Issues* section, select type of relation 'Follows' or 'Precedes' and enter # of the related issue.
40 | 3. Save changes.
41 | 4. Go to *Gantt* tab.
42 |
43 | === Smart sorting
44 |
45 | By default issues on the chart sorted uses improved algorythm. However you can switch back to default Redmine sorting by disabling 'Smart sorting' plugin setting.
46 |
47 | === Rescheduling
48 |
49 | Changing due date of an issue causes rescheduling of related issues. By default, any day of week is a working day, and new start and due dates can fall on weekends as well as on other days. To change this,
50 | go to plugin settings and disable *Work on weekends* checkbox. This will turn on support of working days. So far only normal weekends supported; you need manually change dates falling on holidays.
51 |
52 | New setting *Work on weekends* introduced in v.0.6.
53 |
54 | == Problems and Limitations
55 |
56 | - Currently only '*follows*' and '*precedes*' relations are used to calculate schedule of dependend issues. Other relation types are rendered on the chart, but they *do* *not* *change* schedule of related issues.
57 | - Only 'finish-to-start' dependency type is available.
58 | - One issue can precede many issues but, can be preceeded with only one issue.
59 |
60 | == Helping out
61 |
62 | If you notice any problems, please report them to the GitHub issue tracker {here}[https://github.com/kulesa/redmine_better_gantt_chart/issues]. Feel free to contact me via GitHub or Twitter
63 | or whatever with any other questions or feature requests. To submit changes fork the project and send a pull request.
64 |
65 | === Running specs
66 |
67 | To run specs,
68 |
69 | - go to the plugin folder, uncomment content of Gemfile, run `bundle`
70 | - run `bundle exec rspec`.
71 |
72 | == Contributors
73 |
74 | Thanks to Jeremy Subtil ({BigMadWolf}[https://github.com/BigMadWolf]) for contributing a patch for displaying connections between cross-project related issues.
75 |
76 | == License
77 |
78 | Better Gantt Chart is released under MIT license.
79 |
--------------------------------------------------------------------------------
/assets/javascripts/raphael.arrow.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | /*
3 | This plugin draws arrows on Redmine gantt chart.
4 | */ var __slice = Array.prototype.slice;
5 | Raphael.fn.ganttArrow = function(coords, relationType) {
6 | var L1, M, arrow, arrowhead, cmd, deltaX, deltaY, l2, line, m, relationDash, triangle, x1, x2, x3, x4, x5, x6, y1, y2, y3, y4, y5, y6, _ref, _ref2, _ref3, _ref4, _ref5, _ref6;
7 | if (relationType == null) {
8 | relationType = "follows";
9 | }
10 | relationDash = {
11 | "follows": "",
12 | "duplicated": "- ",
13 | "blocked": "-",
14 | "relates": "."
15 | };
16 | cmd = function() {
17 | var a, cmd;
18 | cmd = arguments[0], a = 2 <= arguments.length ? __slice.call(arguments, 1) : [];
19 | return cmd.concat(" ", a.join(" "), " ");
20 | };
21 | M = function(x, y) {
22 | return cmd("M", x, y);
23 | };
24 | m = function(x, y) {
25 | return cmd("m", x, y);
26 | };
27 | L1 = function(x1, y1) {
28 | return cmd("L", x1, y1);
29 | };
30 | l2 = function(x1, y1, x2, y2) {
31 | return cmd("l", x1, y1, x2, y2);
32 | };
33 | line = function(x1, y1, x2, y2) {
34 | return M(x1, y1) + L1(x2, y2);
35 | };
36 | triangle = function(cx, cy, r) {
37 | r *= 1.5;
38 | return "".concat(M(cx, cy), m(0, -1 * r * .58), l2(r * .5, r * .87, -r, 0), " z");
39 | };
40 | x1 = coords[0], y1 = coords[1], x6 = coords[2], y6 = coords[3];
41 | x1 += 3;
42 | arrow = this.set();
43 | deltaX = 7;
44 | deltaY = 8;
45 | _ref = [x1 + deltaX - 3, y1], x2 = _ref[0], y2 = _ref[1];
46 | _ref2 = [x6 - deltaX, y6], x5 = _ref2[0], y5 = _ref2[1];
47 | if (y1 < y6) {
48 | _ref3 = [x2, y6 - deltaY], x3 = _ref3[0], y3 = _ref3[1];
49 | } else {
50 | _ref4 = [x2, y6 + deltaY], x3 = _ref4[0], y3 = _ref4[1];
51 | }
52 | if (x1 + deltaX + 7 < x6) {
53 | _ref5 = [x3, y5], x4 = _ref5[0], y4 = _ref5[1];
54 | } else {
55 | _ref6 = [x5, y3], x4 = _ref6[0], y4 = _ref6[1];
56 | }
57 | arrow.push(this.path(line(x1, y1, x2, y2)));
58 | arrow.push(this.path(line(x2, y2, x3, y3)));
59 | arrow.push(this.path(line(x3, y3, x4, y4)));
60 | arrow.push(this.path(line(x4, y4, x5, y5)));
61 | arrow.push(this.path(line(x5, y6, x6, y6)));
62 | arrowhead = arrow.push(this.path(triangle(x6 + deltaX - 5, y6 + 1, 5)).rotate(90));
63 | arrow.toFront();
64 | return arrow.attr({
65 | fill: "#444",
66 | stroke: "#222",
67 | "stroke-dasharray": relationDash[relationType]
68 | });
69 | };
70 | /*
71 | Draws connection arrows over the gantt chart
72 | */
73 | window.redrawGanttArrows = function() {
74 | var calculateAnchors, paper, relationAttrs;
75 | paper = Raphael("gantt_lines", "100%", "100%");
76 | paper.clear;
77 | window.paper = paper;
78 | paper.canvas.style.position = "absolute";
79 | paper.canvas.style.zIndex = "24";
80 | relationAttrs = ["follows", "blocked", "duplicated", "relates"];
81 | calculateAnchors = function(from, to) {
82 | var anchors, fromOffsetX, fromOffsetY, toOffsetX, toOffsetY, typeOffsetX, _ref, _ref2;
83 | _ref = [from.position().left, from.position().top], fromOffsetX = _ref[0], fromOffsetY = _ref[1];
84 | _ref2 = [to.position().left, to.position().top], toOffsetX = _ref2[0], toOffsetY = _ref2[1];
85 | if (to.hasClass('parent')) {
86 | typeOffsetX = 10;
87 | } else {
88 | typeOffsetX = 6;
89 | }
90 | anchors = [fromOffsetX + from.width() - 1, fromOffsetY + from.height() / 2, toOffsetX - typeOffsetX, toOffsetY + to.height() / 2];
91 | return anchors;
92 | };
93 | return $('div.task_todo').each(function(element) {
94 | var from, id, item, related, relationAttribute, to, _i, _len, _results;
95 | element = this;
96 | _results = [];
97 | for (_i = 0, _len = relationAttrs.length; _i < _len; _i++) {
98 | relationAttribute = relationAttrs[_i];
99 | _results.push((function() {
100 | var _i, _len, _ref, _results;
101 | if ((related = element.getAttribute(relationAttribute))) {
102 | _ref = related.split(',');
103 | _results = [];
104 | for (_i = 0, _len = _ref.length; _i < _len; _i++) {
105 | id = _ref[_i];
106 | _results.push((item = $('#' + id)) ? (from = item, to = $('#' + element.id), (from.position() != null) && (to.position() != null) ? paper.ganttArrow(calculateAnchors(from, to), relationAttribute) : void 0) : void 0);
107 | }
108 | return _results;
109 | }
110 | })());
111 | }
112 | return _results;
113 | });
114 | };
115 | }).call(this);
116 |
--------------------------------------------------------------------------------
/spec/lib/calendar_spec.rb:
--------------------------------------------------------------------------------
1 | require 'active_support'
2 | require File.expand_path('spec/helpers')
3 | require File.expand_path('../../../lib/redmine_better_gantt_chart/redmine_better_gantt_chart', __FILE__)
4 | require File.expand_path('../../../lib/redmine_better_gantt_chart/calendar', __FILE__)
5 |
6 | describe RedmineBetterGanttChart::Calendar do
7 | include Helpers
8 |
9 | before(:each) do
10 | work_on_weekends true
11 | end
12 |
13 | let!(:thursday) { subject.next_day_of_week(4) }
14 | let!(:friday) { subject.next_day_of_week(5) }
15 | let!(:saturday) { subject.next_day_of_week(6) }
16 |
17 | it "should tell next requested day of week" do
18 | yesterday_date_of_week = Date.yesterday.wday
19 | subject.next_day_of_week(yesterday_date_of_week).should == Date.today + 6.days
20 | end
21 |
22 | it "should tell previous requested day of week" do
23 | tomorrow_day_of_week = Date.tomorrow.wday
24 | subject.previous_day_of_week(tomorrow_day_of_week).should == Date.today - 6.days
25 | end
26 |
27 | it "should have 8 working days between today and 1 week later if work on weekends enabled" do
28 | subject.workdays_between(Date.today, Date.today + 1.week).should == 8
29 | end
30 |
31 | it "should have 4 working days between today and 1 week later if work on weekends disabled" do
32 | work_on_weekends false
33 | subject.workdays_between(Date.today, Date.today + 1.week).should == 6
34 | end
35 |
36 | it "should have 1 working days between the same start and end dates" do
37 | subject.workdays_between(friday, friday).should == 1
38 | end
39 |
40 | it "should have 6 working days between saturday and next_monday if work on weekends disabled" do
41 | work_on_weekends false
42 | subject.workdays_between(saturday, saturday + 9).should == 6
43 | end
44 |
45 | it "should calculate the difference if date to is earlier than date from" do
46 | subject.workdays_between(friday, thursday).should ==
47 | subject.workdays_between(thursday, friday).should
48 | end
49 |
50 | it "should tell next working day is friday if today is friday" do
51 | subject.next_working_day(friday).should == friday
52 | end
53 |
54 | it "should tell next working day is saturday if today is saturday and work on weekends is enabled" do
55 | subject.next_working_day(saturday).should == saturday
56 | end
57 |
58 | it "should tell next working day is monday if today is saturday and work on weekends is disabled" do
59 | work_on_weekends false
60 | subject.next_working_day(saturday).should == saturday + 2.days
61 | end
62 |
63 | it "should tell previous working day is friday if today is friday" do
64 | subject.previous_working_day(friday).should == friday
65 | end
66 |
67 | it "should tell previous working day is saturday if today is saturday and work on weekends is enabled" do
68 | subject.previous_working_day(saturday).should == saturday
69 | end
70 |
71 | it "should tell previous working day is friday if today is sunday and work on weekends is disabled" do
72 | work_on_weekends false
73 | subject.previous_working_day(saturday + 1.day).should == friday
74 | end
75 |
76 | it "should let calculate finish date based on duration and start date with work on weekends enabled" do
77 | subject.workdays_from_date(Date.today, 7).should == Date.today + 1.week
78 | end
79 |
80 | it "should let calculate finish date based on duration and start date wihout work on weekends" do
81 | work_on_weekends false
82 | subject.workdays_from_date(Date.today, 5).should == Date.today + 1.week
83 | end
84 |
85 | it "should calculate start date based on duration and finish date" do
86 | work_on_weekends false
87 | previous_friday = friday - 7
88 | previous_monday = friday - 4
89 | subject.workdays_from_date(friday, -4).should == previous_monday
90 | subject.workdays_from_date(friday, -6).should == previous_friday
91 | end
92 |
93 | it "should calculate workdays before date" do
94 | subject.should_receive(:workdays_from_date).with(Date.today, -5)
95 | subject.workdays_before_date(Date.today, 5)
96 | end
97 |
98 | it "should say that 6 working days from saturday is monday if work on weekends disabled" do
99 | work_on_weekends false
100 | next_monday = saturday + 9.days
101 | subject.workdays_from_date(saturday, 6).should == next_monday
102 | end
103 |
104 | it "should say that 6 working from sunday is monday if work on weekends disabled" do
105 | work_on_weekends false
106 | sunday = saturday + 1.day
107 | next_monday = sunday + 8.days
108 | subject.workdays_from_date(sunday, 6).should == next_monday
109 | end
110 |
111 | it "should return the same day if duration is 0" do
112 | work_on_weekends false
113 | subject.workdays_from_date(friday, 0).should == friday
114 | end
115 | end
116 |
--------------------------------------------------------------------------------
/lib/redmine_better_gantt_chart/issue_dependency_patch.rb:
--------------------------------------------------------------------------------
1 | module RedmineBetterGanttChart
2 | module IssueDependencyPatch
3 | def self.included(base) # :nodoc:
4 | base.send(:include, InstanceMethods)
5 |
6 | base.class_eval do
7 | alias_method_chain :reschedule_following_issues, :fast_update
8 | alias_method_chain :reschedule_on!, :earlier_date
9 | alias_method_chain :soonest_start, :dependent_parent_validation
10 | alias_method_chain :duration, :work_days
11 | end
12 | end
13 |
14 | module InstanceMethods
15 |
16 | def create_journal_entry
17 | create_journal
18 | end
19 |
20 | # Redefined to work without recursion on AR objects
21 | def reschedule_following_issues_with_fast_update
22 | if start_date_changed? || due_date_changed?
23 | cache_and_apply_changes do
24 | reschedule_dependent_issue
25 | end
26 | end
27 | end
28 |
29 | def cache_and_apply_changes(&block)
30 | @changes = {} # a hash of changes to be applied later, will contain values like this: { issue_id => {:start_date => ..., :end_date => ...}}
31 | @parents = {} # a hash of children for any affected parent issue
32 |
33 | yield
34 |
35 | reschedule_parents
36 | ordered_changes = prepare_and_sort_changes_list(@changes)
37 |
38 | Issue.skip_callbacks do
39 | transaction do
40 | ordered_changes.each do |the_changes|
41 | issue_id, changes = the_changes[1]
42 | apply_issue_changes(issue_id, changes)
43 | end
44 | end
45 | end
46 | end
47 |
48 | def prepare_and_sort_changes_list(changes_list)
49 | ordered_changes = []
50 | changes_list.each do |c|
51 | ordered_changes << [c[1][:due_date] || c[1][:start_date], c]
52 | end
53 | ordered_changes.sort!
54 | end
55 |
56 | def apply_issue_changes(issue_id, changes)
57 | issue = Issue.find(issue_id)
58 | changes.each_pair do |key, value|
59 | changes.delete(key) if issue.send(key) == value.to_date
60 | end
61 | unless changes.empty?
62 | issue.init_journal(User.current, I18n.t('task_moved_journal_entry', id: self.id ) )
63 | issue.update_attributes(changes)
64 | issue.create_journal_entry
65 | end
66 | end
67 |
68 | def reschedule_dependent_issue(issue = self, options = {}) #start_date_to = nil, due_date_to = nil
69 | cache_change(issue, options)
70 | process_child_issues(issue) if !issue.leaf?
71 | process_following_issues(issue)
72 | update_parent_start_and_due(issue) if issue.parent_id
73 | end
74 |
75 | def process_child_issues(issue)
76 | childs_with_nil_start_dates = []
77 |
78 | issue.leaves.each do |leaf|
79 | start_date = cached_value(issue, :start_date)
80 | child_start_date = cached_value(leaf, :start_date)
81 |
82 | cache_change(issue, :start_date => child_start_date) if start_date.nil?
83 |
84 | if child_start_date.nil? or
85 | (start_date > child_start_date) or
86 | (start_date < child_start_date and issue.start_date == leaf.start_date)
87 | reschedule_dependent_issue(leaf, :start_date => start_date)
88 | end
89 | end
90 | end
91 |
92 | def process_following_issues(issue)
93 | issue.relations_from.each do |relation|
94 | if is_a_link_with_following_issue?(relation) && due_date = cached_value(issue, :due_date)
95 | new_start_date = RedmineBetterGanttChart::Calendar.workdays_from_date(due_date, relation.delay) + 1.day
96 | new_start_date = RedmineBetterGanttChart::Calendar.next_working_day(new_start_date)
97 | reschedule_dependent_issue(relation.issue_to, :start_date => new_start_date)
98 | end
99 | end
100 | end
101 |
102 | def is_a_link_with_following_issue?(relation)
103 | relation.issue_to && relation.relation_type == IssueRelation::TYPE_PRECEDES
104 | end
105 |
106 | def reschedule_parents
107 | @parents.dup.each_pair do |parent_id, children|
108 | parent_min_start = min_parent_start(parent_id)
109 | parent_max_start = max_parent_due(parent_id)
110 | cache_change(parent_id, :start_date => parent_min_start,
111 | :due_date => parent_max_start)
112 |
113 | children.each do |child| # If parent's start is changing, change start_date of any childs that have empty start_date
114 | if cached_value(child, :start_date).nil?
115 | cache_change(child, :start_date => parent_min_start, :parent => true)
116 | end
117 | end
118 |
119 | process_following_issues(Issue.find(parent_id))
120 | end
121 | end
122 |
123 | # Caches changes to be applied later. If no attributes to change given, just caches current values.
124 | # Use :parent => true to just change one date without changing the other. If :parent is not specified,
125 | # change of one of the issue dates will cause change of the other.
126 | #
127 | # If no options is provided existing, issue cache is initialized.
128 | def cache_change(issue, options = {})
129 | if issue.is_a?(Integer)
130 | issue_id = issue
131 | issue = Issue.find(issue_id) unless options[:start_date] && options[:due_date] # optimization for the case when issue is not required
132 | else
133 | issue_id = issue.id
134 | end
135 |
136 | @changes[issue_id] ||= {}
137 | new_dates = {}
138 |
139 | if options.empty? || (options[:start_date] && options[:due_date])
140 | # Both or none dates changed
141 | [:start_date, :due_date].each do |attr|
142 | new_dates[attr] = options[attr] || @changes[issue_id][attr] || issue.send(attr)
143 | end
144 | else
145 | # One of the dates changed - change another accordingly
146 | changed_attr = options[:start_date] && :start_date || :due_date
147 | other_attr = if changed_attr == :start_date then :due_date else :start_date end
148 |
149 | new_dates[changed_attr] = options[changed_attr]
150 | if issue.send(other_attr)
151 | if options[:parent]
152 | new_dates[other_attr] = issue.send(other_attr)
153 | else
154 | new_dates[other_attr] = RedmineBetterGanttChart::Calendar.workdays_from_date(new_dates[changed_attr], RedmineBetterGanttChart::Calendar.workdays_between(issue.send(changed_attr), issue.send(other_attr)).to_i - 1) end
155 | end
156 | end
157 |
158 | [:start_date, :due_date].each do |attr|
159 | @changes[issue_id][attr] = new_dates[attr].to_date if new_dates[attr]
160 | end
161 | end
162 |
163 | # Returns cached value or caches it if it hasn't been cached yet
164 | def cached_value(issue, attr)
165 | issue_id = issue.is_a?(Integer) ? issue : issue.id
166 | cache_change(issue_id) unless @changes[issue_id]
167 | @changes[issue_id][attr] || @changes[issue_id][:start_date]
168 | end
169 |
170 | # Each time we update cache of a child issue, need to update cache of the parent issue
171 | # by setting start_date to min(parent.all_children) and due_date to max(parent.all_children).
172 | # Apparently, to do so, first we need to add to cache all child issues of the parent, even if
173 | # they are not affected by rescheduling.
174 | def update_parent_start_and_due(issue)
175 | current_parent_id = issue.parent_id
176 | unless @parents[current_parent_id]
177 | # This parent is touched for the first time, let's cache it's children
178 | @parents[current_parent_id] = [issue.id] # at least the current issue is a child - even if it is not saved yet (is is possible?)
179 | issue.parent.children.each do |child|
180 | cache_change(child) unless @changes[child]
181 | @parents[current_parent_id] << child.id
182 | end
183 | end
184 | end
185 |
186 | def min_parent_start(current_parent_id)
187 | @parents[current_parent_id].uniq.inject(Date.new(5000)) do |min, child_id| # Someone needs to update this before 01/01/5000
188 | min = min < (current_child_start = cached_value(child_id, :start_date)) ? min : current_child_start rescue min
189 | end
190 | end
191 |
192 | def max_parent_due(current_parent_id)
193 | @parents[current_parent_id].uniq.inject(Date.new) do |max, child_id|
194 | max = max > (current_child_due = cached_value(child_id, :due_date)) ? max : current_child_due rescue max
195 | end
196 | end
197 |
198 | # Returns the time scheduled for this issue in working days.
199 | #
200 | def duration_with_work_days
201 | if self.start_date && self.due_date
202 | RedmineBetterGanttChart::Calendar.workdays_between(self.start_date, self.due_date)
203 | else
204 | 0
205 | end
206 | end
207 |
208 | # Changes behaviour of reschedule_on method
209 | def reschedule_on_with_earlier_date!(date)
210 | return if date.nil?
211 |
212 | if start_date.blank? || start_date != date
213 | if due_date.present?
214 | self.due_date = RedmineBetterGanttChart::Calendar.workdays_from_date(date, duration - 1)
215 | end
216 | self.start_date = date
217 | save
218 | end
219 | end
220 |
221 | # Modifies validation of soonest start date for a new task:
222 | # if parent task has dependency, start date cannot be earlier than start date of the parent.
223 | def soonest_start_with_dependent_parent_validation
224 | @soonest_start ||= (
225 | relations_to.collect{|relation| relation.successor_soonest_start} +
226 | ancestors.collect(&:soonest_start) +
227 | [parent_start_constraint]
228 | ).compact.max
229 | end
230 |
231 | # Returns [soonest_start_date] if parent task has dependency contstraints
232 | # or [nil] otherwise
233 | def parent_start_constraint
234 | if parent_issue_id && @parent_issue
235 | @parent_issue.soonest_start
236 | else
237 | nil
238 | end
239 | end
240 | end
241 | end
242 | end
243 |
--------------------------------------------------------------------------------
/spec/models/issue_patch_spec.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path('../../spec_helper', __FILE__)
2 |
3 | describe 'Improved issue dependencies management' do
4 | context 'with work on weekends enabled' do
5 | before(:all) do
6 | work_on_weekends true
7 | end
8 |
9 | before(:each) do
10 | @first_issue, @second_issue = create_related_issues("precedes")
11 | end
12 |
13 | it "issue duration is in calendar days" do
14 | issue = Factory(:issue, :start_date => Date.today, :due_date => Date.today + 1.week)
15 | issue.duration.should == 8
16 | end
17 |
18 | it 'changes date of the dependent task when due date of the first task is moved forward' do
19 | lambda {
20 | @first_issue.due_date = @first_issue.due_date + 2.days
21 | @first_issue.save!
22 | @second_issue.reload
23 | }.should change(@second_issue, :start_date).to(@first_issue.due_date + 3.days)
24 | end
25 |
26 | it 'changes date of the dependent task when due date of the first task is moved back' do
27 | lambda {
28 | @first_issue.due_date = @first_issue.due_date - 2.days
29 | @first_issue.save!
30 | @second_issue.reload
31 | }.should change(@second_issue, :start_date).to(@first_issue.due_date - 1.day)
32 | end
33 |
34 | it 'doesn\'t allow set start date earlier than parent.soonest_start' do
35 | child_issue = Factory.build(:issue)
36 | child_issue.parent_issue_id = @second_issue.id
37 | expect {
38 | child_issue.start_date = @second_issue.start_date - 1
39 | child_issue.save!
40 | }.to raise_error(ActiveRecord::RecordInvalid)
41 | end
42 |
43 | it "doesn't fail when removing an only child issue from the parent" do
44 | parent_issue, next_issue = create_related_issues("precedes")
45 | child_issue = Factory(:issue)
46 | child_issue.parent_issue_id = parent_issue.id
47 | child_issue.save!
48 | parent_issue.reload
49 |
50 | expect {
51 | child_issue.parent_issue_id = nil
52 | child_issue.save!
53 | }.not_to raise_error()
54 | end
55 |
56 | it "doesn't fail when assigning start_date to a child issue when parent's start_date is empty and siblings' start_dates are empty" do
57 | parent_issue = Factory(:issue, :start_date => nil, :due_date => nil)
58 | child_issue1 = Factory(:issue, :start_date => nil, :due_date => nil)
59 | child_issue2 = Factory(:issue, :start_date => nil, :due_date => nil)
60 | child_issue1.parent_issue_id = parent_issue.id
61 | child_issue2.parent_issue_id = parent_issue.id
62 | child_issue1.save!
63 | child_issue2.save!
64 | parent_issue.reload
65 |
66 | expect {
67 | child_issue1.start_date = Date.today
68 | child_issue1.save!
69 | }.not_to raise_error()
70 | end
71 |
72 | it "doesn't fail when start_date of an issue deep in hierarchy is changed from empty" do
73 | parent_issue = Factory(:issue, :start_date => nil, :due_date => nil)
74 | child_issue1 = Factory(:issue, :start_date => nil, :due_date => nil)
75 | child_issue2 = Factory(:issue, :start_date => nil, :due_date => nil)
76 | child_issue1.parent_issue_id = parent_issue.id
77 | child_issue2.parent_issue_id = parent_issue.id
78 | child_issue1.save!
79 | child_issue2.save!
80 |
81 | child_issue1_1 = Factory(:issue, :start_date => nil, :due_date => nil)
82 | child_issue1_2 = Factory(:issue, :start_date => nil, :due_date => nil)
83 | child_issue1_1.parent_issue_id = child_issue1.id
84 | child_issue1_2.parent_issue_id = child_issue1.id
85 | child_issue1_1.save!
86 | child_issue1_2.save!
87 |
88 | child_issue2_1 = Factory(:issue, :start_date => nil, :due_date => nil)
89 | child_issue2_2 = Factory(:issue, :start_date => nil, :due_date => nil)
90 | child_issue2_1.parent_issue_id = child_issue2.id
91 | child_issue2_2.parent_issue_id = child_issue2.id
92 | child_issue2_1.save!
93 | child_issue2_2.save!
94 |
95 | expect {
96 | child_issue2_2.start_date = Date.today
97 | child_issue2_2.save!
98 | }.not_to raise_error()
99 | end
100 |
101 | it "doesn't fail when an issue without start or due date becomes a parent issue" do
102 | parent_issue = Factory(:issue, :start_date => nil, :due_date => nil)
103 | child_issue = Factory(:issue, :due_date => nil)
104 |
105 | expect {
106 | child_issue.parent_issue_id = parent_issue.id
107 | child_issue.save!
108 | }.not_to raise_error()
109 | end
110 |
111 | describe 'handles long dependency chains' do
112 | before do
113 | @start_issue = Factory(:issue)
114 | @current_issue = @start_issue
115 | # Change X.times to a really big number to stress test rescheduling of a really long chain of dependent issues :)
116 | 3.times do
117 | previous_issue, @current_issue = create_related_issues("precedes", @current_issue)
118 | end
119 | end
120 |
121 | it 'and reschedules tens of related issues when due date of the first issue is moved back' do
122 | si = Issue.find(@start_issue.id + 1)
123 | lambda {
124 | @start_issue.due_date = @start_issue.due_date - 2.days
125 | @start_issue.save!
126 | @current_issue.reload
127 | }.should change(@current_issue, :start_date).to(@current_issue.start_date - 2.days)
128 | end
129 |
130 | it 'and reschedules tens of related issues when due date of the first task is moved forth' do
131 | lambda {
132 | @start_issue.due_date = @start_issue.due_date + 2.days
133 | @start_issue.save!
134 | @current_issue.reload
135 | }.should change(@current_issue, :start_date).to(@current_issue.start_date + 2.days)
136 | end
137 |
138 | it 'and doesn\'t allow create circular dependencies' do
139 | expect {
140 | create_related_issues("precedes", @current_issue, @start_issue)
141 | }.to raise_error(ActiveRecord::RecordInvalid)
142 | end
143 | end
144 |
145 | describe 'allows fast rescheduling of dependent issues' do
146 | before do
147 | # Testing on the following dependency chain:
148 | # @initial -> @related -> @parent [ @child1 -> @child2]
149 | @initial, @related = create_related_issues("precedes")
150 | @related, @parent = create_related_issues("precedes", @related)
151 | @child1 = Factory.build(:issue, :start_date => @parent.start_date)
152 | @child2 = Factory.build(:issue, :start_date => @parent.start_date)
153 | @child1.parent_issue_id = @parent.id
154 | @child1.save!
155 | @child2.parent_issue_id = @parent.id
156 | @child2.save!
157 |
158 | create_related_issues("precedes", @child1, @child2)
159 | end
160 |
161 | it "should change start date of the last dependend child issue when due date of the first issue moved FORWARD" do
162 | lambda {
163 | @initial.due_date = @initial.due_date + 2.days
164 | @initial.save!
165 | @child2.reload
166 | }.should change(@child2, :start_date).to(@child2.start_date + 2.days)
167 | end
168 |
169 | it "should change start date of the last dependend child issue when due date of the first issue moved BACK" do
170 | lambda {
171 | @initial.due_date = @initial.due_date - 2.days
172 | @initial.save!
173 | @child2.reload
174 | }.should change(@child2, :start_date).to(@child2.start_date - 2.days)
175 | end
176 |
177 | it "should not fail when due_date of one of rescheduled issues is nil" do
178 | initial, child = create_related_issues("precedes")
179 | parent = Factory(:issue, :due_date => nil)
180 | other_child = Factory(:issue, :due_date => nil)
181 | child.parent_issue_id = parent.id
182 | child.save!
183 | other_child.parent_issue_id = parent.id
184 | other_child.save!
185 | parent.reload
186 | other_child.reload
187 | child.destroy
188 | end
189 |
190 | it "should reschedule start date of parent task of a dependend child task" do
191 | parent_a = Factory(:issue)
192 | child_a = Factory.build(:issue, :start_date => parent_a.start_date)
193 | child_b = Factory.build(:issue, :start_date => parent_a.start_date)
194 | child_a.parent_issue_id = parent_a.id
195 | child_b.parent_issue_id = parent_a.id
196 | child_a.save!
197 | child_b.save!
198 |
199 | child_a, child_b = create_related_issues("precedes", child_a, child_b)
200 |
201 | parent_b = Factory(:issue)
202 | child_c = Factory.build(:issue, :start_date => parent_b.start_date)
203 | child_d = Factory.build(:issue, :start_date => parent_b.start_date)
204 | child_c.parent_issue_id = parent_b.id
205 | child_d.parent_issue_id = parent_b.id
206 | child_c.save!
207 | child_d.save!
208 |
209 | child_b, child_c = create_related_issues("precedes", child_b, child_c)
210 | child_c, child_d = create_related_issues("precedes", child_c, child_d)
211 |
212 | parent_b.reload
213 |
214 | parent_start = parent_b.start_date
215 | parent_due = parent_b.due_date
216 |
217 | child_a.due_date = child_a.due_date - 2.days
218 | child_a.save!
219 | parent_b.reload
220 |
221 | parent_b.start_date.should == parent_start - 2.days
222 | parent_b.due_date.should == parent_due - 2.days
223 | end
224 |
225 | it "should reshedule a following task of a parent task, when the parent task itself being rescheduled after changes in it's child task" do
226 | parent = Factory(:issue)
227 | child = Factory(:issue, :start_date => Date.today,
228 | :due_date => Date.today + 1,
229 | :parent_issue_id => parent.id)
230 | follower = Factory(:issue, :start_date => Date.today, :due_date => Date.today + 1)
231 | relate_issues parent, follower
232 |
233 | child.update_attributes(:due_date => Date.today + 7)
234 | parent.reload
235 | follower.reload
236 |
237 | follower.start_date.should == parent.due_date + 1
238 | end
239 | end
240 | end
241 |
242 | context "when work on weekends is disabled" do
243 | before(:all) do
244 | work_on_weekends false
245 | end
246 |
247 | it "issue duration is in working days" do
248 | issue = Factory(:issue, :start_date => Date.today, :due_date => Date.today + 1.week)
249 | issue.duration.should == 6
250 | end
251 |
252 | it "should reschedule after with earlier date" do
253 | monday = RedmineBetterGanttChart::Calendar.next_day_of_week(1)
254 | @first_issue = Factory(:issue, :start_date => monday, :due_date => monday + 1)
255 | @second_issue = Factory(:issue, :start_date => monday, :due_date => monday + 1)
256 |
257 | lambda {
258 | relate_issues(@first_issue, @second_issue)
259 | }.should change(@second_issue, :due_date).to(monday + 3)
260 |
261 | end
262 |
263 | it "lets create a relation between issues without due dates" do
264 | pop = Factory(:issue)
265 | parent_issue = Factory(:issue, :start_date => Date.today, :due_date => nil)
266 | issue1 = Factory(:issue, :start_date => Date.today, :due_date => nil)
267 | issue1.update_attributes!(:parent_issue_id => parent_issue.id)
268 | issue2 = Factory(:issue, :start_date => Date.today, :due_date => nil)
269 | issue2.update_attributes!(:parent_issue_id => parent_issue.id)
270 | relate_issues(issue2, issue1, "follows")
271 |
272 | issue1.reload
273 | issue1.relations.count.should == 1
274 | end
275 |
276 | end
277 | end
278 |
--------------------------------------------------------------------------------
/app/views/gantts/show.html.erb:
--------------------------------------------------------------------------------
1 | <%= javascript_include_tag "raphael-min.js", :plugin => "redmine_better_gantt_chart" %>
2 | <%= javascript_include_tag "raphael.arrow.js", :plugin => "redmine_better_gantt_chart" %>
3 | <% @gantt.view = self %>
4 | 19 | <%= gantt_zoom_link(@gantt, :in) %> 20 | <%= gantt_zoom_link(@gantt, :out) %> 21 |
22 | 23 | 35 | <% end %> 36 | 37 | <%= error_messages_for 'query' %> 38 | <% if @query.valid? %> 39 | <% 40 | zoom = 1 41 | @gantt.zoom.times { zoom = zoom * 2 } 42 | 43 | subject_width = 330 44 | header_heigth = 18 45 | line_height = 18 46 | 47 | show_years = true 48 | show_weeks = false 49 | show_days = false 50 | show_day_numbers = false 51 | 52 | headers_height = 2 * header_heigth 53 | if @gantt.zoom > 1 54 | show_years = false 55 | show_weeks = true 56 | if @gantt.zoom > 2 57 | show_days = true 58 | headers_height = (3 * header_heigth).to_i 59 | if @gantt.zoom > 3 60 | show_day_numbers = true 61 | headers_height = (3.25 * header_heigth).to_i 62 | end 63 | end 64 | end 65 | 66 | # Width of the entire chart 67 | g_width = ((@gantt.work_days_in(@gantt.date_to, @gantt.date_from) + 1) * zoom).to_i 68 | @gantt.render(:top => headers_height + 8, 69 | :zoom => zoom, 70 | :g_width => g_width, 71 | :subject_width => subject_width) 72 | g_height = [(line_height * (@gantt.number_of_rows + 2)) + 0, 208 ].max 73 | t_height = g_height + headers_height; 74 | s_height = 24; 75 | %> 76 | 77 | <% if @gantt.truncated %> 78 |<%= l(:notice_gantt_chart_truncated, :max => @gantt.max_rows) %>
79 | <% end %> 80 | 81 || 84 | <% 85 | style = "" 86 | style += "position:relative;" 87 | style += "height: #{t_height + s_height}px;" 88 | style += "width: #{subject_width + 1}px;" 89 | %> 90 | <%= content_tag(:div, :style => style) do %> 91 | <% 92 | style = "" 93 | style += "right:-2px;" 94 | style += "width: #{subject_width}px;" 95 | style += "height: #{headers_height}px;" 96 | style += 'background: #eee;' 97 | %> 98 | <%= content_tag(:div, "", :style => style, :class => "gantt_hdr") %> 99 | <% 100 | style = "" 101 | style += "right:-2px;" 102 | style += "width: #{subject_width}px;" 103 | style += "height: #{t_height}px;" 104 | style += 'border-left: 1px solid #c0c0c0;' 105 | style += 'overflow: hidden;' 106 | %> 107 | <%= content_tag(:div, "", :style => style, :class => "gantt_hdr") %> 108 | <%= content_tag(:div, :class => "gantt_subjects") do %> 109 | <%= @gantt.subjects.html_safe %> 110 | <% end %> 111 | <% end %> 112 | | 113 | 114 |
115 |
116 | <%
117 | style = ""
118 | style += "width: #{g_width - 1}px;"
119 | style += "height: #{headers_height}px;"
120 | style += 'background: #eee;'
121 | %>
122 | <%= content_tag(:div, ' '.html_safe, :style => style, :class => "gantt_hdr") %>
123 |
124 | <% ###### Today red line (excluded from cache) ###### %>
125 | <% if Date.today >= @gantt.date_from and Date.today <= @gantt.date_to %>
126 | <%
127 | width = zoom
128 | today_left = (((@gantt.work_days_in(Date.today, @gantt.date_from) + 0) * zoom).floor() - 1).to_i
129 | style = ""
130 | style += "position: absolute;"
131 | style += "height: #{headers_height + g_height}px;"
132 | style += "top: 0px;"
133 | style += "left: #{today_left}px;"
134 | style += "width: #{width}px;"
135 | #style += "border-left: 1px dashed red;"
136 | style += 'background:#ffe3e3;'
137 | %>
138 | <%= content_tag(:div, ' '.html_safe, :style => style) %>
139 | <% end %>
140 |
141 | <% ###### Years headers ###### %>
142 | <% if show_years %>
143 | <%
144 | left = 0
145 | height = (show_weeks ? header_heigth : header_heigth + g_height)
146 | years = 1
147 | months_remaining = @gantt.months - (12 - @gantt.date_from.month) - 1
148 | years += months_remaining <= 0 ? 0 : (months_remaining / 12).to_i + 1
149 | %>
150 | <% years.times do |year| %>
151 | <%
152 | year = year + @gantt.date_from.year.to_i
153 | if year == @gantt.date_from.year
154 | work_days = @gantt.work_days_in(Date.new(year + 1), @gantt.date_from)
155 | months_remaining -= 12 - (@gantt.date_from.month).to_i - 1
156 | elsif months_remaining < 12
157 | work_days = @gantt.work_days_in(Date.new(year, months_remaining+1, 1), Date.new(year))
158 | months_remaining = 0
159 | else
160 | work_days = @gantt.work_days_in(Date.new(year + 1), Date.new(year))
161 | months_remaining -= 12
162 | end
163 | width = (work_days * zoom - 1).to_i
164 | style = ""
165 | style += "left: #{left}px;"
166 | style += "width: #{width}px;"
167 | style += "height: #{height}px;"
168 | %>
169 | <%= content_tag(:div, :style => style, :class => "gantt_hdr") do %>
170 | <%= link_to h("#{year}"),
171 | @gantt.params.merge(:year => year),
172 | :title => "#{year}" %>
173 | <% end %>
174 | <%
175 | left = left + width + 1
176 | %>
177 | <% end %>
178 | <% end %>
179 |
180 | <% ###### Months headers ###### %>
181 | <%
182 | month_f = @gantt.date_from
183 | left = 0
184 | height = (show_weeks ? header_heigth : header_heigth + g_height)
185 | %>
186 | <% @gantt.months.times do %>
187 | <%
188 | width = (@gantt.work_days_in(month_f >> 1, month_f) * zoom - 1).to_i
189 | style = ""
190 | style += "top: #{header_heigth+1}px;" if show_years
191 | style += "left: #{left}px;"
192 | style += "width: #{width}px;"
193 | style += "height: #{height}px;"
194 | %>
195 | <%= content_tag(:div, :style => style, :class => "gantt_hdr") do %>
196 | <%
197 | month_title = month_f.strftime("%b")
198 | month_title += " #{month_f.year}" unless show_years
199 | %>
200 | <%= link_to h("#{month_title}"),
201 | @gantt.params.merge(:year => month_f.year, :month => month_f.month),
202 | :title => "#{month_name(month_f.month)} #{month_f.year}" %>
203 | <% end %>
204 | <%
205 | left = left + width + 1
206 | month_f = month_f >> 1
207 | %>
208 | <% end %>
209 |
210 | <% ###### Weeks headers ###### %>
211 | <% if show_weeks %>
212 | <%
213 | work_days_in_week = @gantt.work_on_weekends ? 7 : 5
214 | left = 0
215 | height = (show_days ? header_heigth - 1 : header_heigth - 1 + g_height)
216 | %>
217 | <% if @gantt.date_from.cwday == 1 %>
218 | <%
219 | # @date_from is monday
220 | week_f = @gantt.date_from
221 | %>
222 | <% else %>
223 | <%
224 | # find next monday after @date_from
225 | week_f = @gantt.date_from + (7 - @gantt.date_from.cwday + 1)
226 | width = (work_days_in_week - @gantt.date_from.cwday + 1) * zoom - 1
227 | style = ""
228 | style += "left: #{left}px;"
229 | style += "top: #{header_heigth+1}px;"
230 | style += "width: #{width}px;"
231 | style += "height: #{height}px;"
232 | %>
233 | <%= content_tag(:div, ' '.html_safe,
234 | :style => style, :class => "gantt_hdr") %>
235 | <% left = left + width + 1 %>
236 | <% end %>
237 | <% while week_f <= @gantt.date_to %>
238 | <%
239 | width = ((week_f + work_days_in_week - 1 <= @gantt.date_to) ?
240 | work_days_in_week * zoom - 1 :
241 | (@gantt.date_to - week_f + 1) * zoom - 1).to_i
242 | style = ""
243 | style += "left: #{left}px;"
244 | style += "top: #{header_heigth+1}px;"
245 | style += "width: #{width}px;"
246 | style += "height: #{height}px;"
247 | %>
248 | <%= content_tag(:div, :style => style, :class => "gantt_hdr") do %>
249 | <%= content_tag(:small) do %>
250 | <%= week_f.cweek if width >= 16 %>
251 | <% end %>
252 | <% end %>
253 | <%
254 | left = left + width + 1
255 | week_f = week_f + 7
256 | %>
257 | <% end %>
258 | <% end %>
259 |
260 | <% ###### Days headers ####### %>
261 | <% if show_days %>
262 | <%
263 | left = 0
264 | height = g_height + header_heigth - 1
265 | wday = @gantt.date_from.cwday
266 | %>
267 | <% (@gantt.date_from).upto(@gantt.date_to) do |day| %>
268 | <% if (day.cwday <= work_days_in_week) %>
269 | <%
270 | width = zoom - 1
271 | day_abbr = [:"zh", :"zh-TW"].include?(current_language) ? day_name(wday)[6,3] : day_name(wday).first # correct abbreviation of day of week for Chinese language
272 | style = ""
273 | style += "left: #{left}px;"
274 | style += "top: #{(header_heigth*2)+1}px;"
275 | style += "width: #{width}px;"
276 | style += "height: #{height}px;"
277 | style += "text-align:center;"
278 | style += "font-size:0.7em;"
279 | style += 'background:#f1f1f1;' if (@gantt.work_on_weekends && day.cwday > 5)
280 | style += 'border-left: 1px solid #c6c6c6;' if (!@gantt.work_on_weekends && day.cwday == 1)
281 | %>
282 | <%= content_tag(:div, :style => style, :class => "gantt_hdr") do %>
283 | <%= day_letter(wday) %>
284 | <% if show_day_numbers %>
285 |
304 | 286 | <%= (day).mday %> 287 | <% end %> 288 | <% end %> 289 | <% end %> 290 | <% 291 | if day.cwday <= work_days_in_week 292 | left = left + width+1 293 | wday = wday + 1 294 | else 295 | wday = 1 296 | end 297 | %> 298 | <% end %> 299 | <% end %> 300 | 301 | <%= @gantt.lines.html_safe %> 302 | 303 | |
305 |
| 311 | <%= link_to_content_update("\xc2\xab " + l(:label_previous), 312 | params.merge(@gantt.params_previous)) %> 313 | | 314 |315 | <%= link_to_content_update(l(:label_next) + " \xc2\xbb", 316 | params.merge(@gantt.params_next)) %> 317 | | 318 |
1){x=y.sqrt(x);c=x*c;d=x*d}var z=c*c,A=d*d,C=(f==g?-1:1)*y.sqrt(B((z*A-z*u*u-A*t*t)/(z*u*u+A*t*t))),E=C*c*u/d+(a+h)/2,F=C*-d*t/c+(b+i)/2,G=y.asin(((b-F)/d).toFixed(9)),H=y.asin(((i-F)/d).toFixed(9));G=a e){if(c&&!l.start){m=cq(g,h,i[1],i[2],i[3],i[4],i[5],i[6],e-n);k+=["C",m.start.x,m.start.y,m.m.x,m.m.y,m.x,m.y];if(f)return k;l.start=k;k=["M",m.x,m.y+"C",m.n.x,m.n.y,m.end.x,m.end.y,i[5],i[6]][v]();n+=j;g=+i[5];h=+i[6];continue}if(!b&&!c){m=cq(g,h,i[1],i[2],i[3],i[4],i[5],i[6],e-n);return{x:m.x,y:m.y,alpha:m.alpha}}}n+=j;g=+i[5];h=+i[6]}k+=i}l.end=k;m=b?n:c?l:a.findDotsAtSegment(g,h,i[1],i[2],i[3],i[4],i[5],i[6],1);m.alpha&&(m={x:m.x,y:m.y,alpha:m.alpha});return m}},cs=cr(1),ct=cr(),cu=cr(0,1);bO.getTotalLength=function(){if(this.type!="path")return;if(this.node.getTotalLength)return this.node.getTotalLength();return cs(this.attrs.path)};bO.getPointAtLength=function(a){if(this.type!="path")return;return ct(this.attrs.path,a)};bO.getSubpath=function(a,b){if(this.type!="path")return;if(B(this.getTotalLength()-b)<"1e-6")return cu(this.attrs.path,a).end;var c=cu(this.attrs.path,b,1);return a?cu(c,a).end:c};a.easing_formulas={linear:function(a){return a},"<":function(a){return C(a,3)},">":function(a){return C(a-1,3)+1},"<>":function(a){a=a*2;if(a<1)return C(a,3)/2;a-=2;return(C(a,3)+2)/2},backIn:function(a){var b=1.70158;return a*a*((b+1)*a-b)},backOut:function(a){a=a-1;var b=1.70158;return a*a*((b+1)*a+b)+1},elastic:function(a){if(a==0||a==1)return a;var b=0.3,c=b/4;return C(2,-10*a)*y.sin((a-c)*(2*D)/b)+1},bounce:function(a){var b=7.5625,c=2.75,d;if(a<1/c)d=b*a*a;else if(a<2/c){a-=1.5/c;d=b*a*a+0.75}else if(a<2.5/c){a-=2.25/c;d=b*a*a+0.9375}else{a-=2.625/c;d=b*a*a+0.984375}return d}};var cv=[],cw=function(){var b=+(new Date);for(var c=0;c