/
issue_dependency_patch.rb
235 lines (202 loc) · 9.02 KB
/
issue_dependency_patch.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
module RedmineBetterGanttChart
module IssueDependencyPatch
def self.included(base) # :nodoc:
base.send(:include, InstanceMethods)
base.class_eval do
alias_method_chain :reschedule_following_issues, :fast_update
alias_method_chain :reschedule_after, :earlier_date
alias_method_chain :soonest_start, :dependent_parent_validation
alias_method_chain :duration, :work_days
end
end
module InstanceMethods
# Redefined to work without recursion on AR objects
def reschedule_following_issues_with_fast_update
if start_date_changed? || due_date_changed?
cache_and_apply_changes do
reschedule_dependent_issue
end
end
end
def cache_and_apply_changes(&block)
@changes = {} # a hash of changes to be applied later, will contain values like this: { issue_id => {:start_date => ..., :end_date => ...}}
@parents = {} # a hash of children for any affected parent issue
yield
reschedule_parents
ordered_changes = prepare_and_sort_changes_list(@changes)
Issue.with_all_callbacks_disabled do
transaction do
ordered_changes.each do |the_changes|
issue_id, changes = the_changes[1]
apply_issue_changes(issue_id, changes)
end
end
end
end
def prepare_and_sort_changes_list(changes_list)
ordered_changes = []
changes_list.each do |c|
ordered_changes << [c[1][:due_date] || c[1][:start_date], c]
end
ordered_changes.sort!
end
def apply_issue_changes(issue_id, changes)
issue = Issue.find(issue_id)
changes.each_pair do |key, value|
changes.delete(key) if issue.send(key) == value.to_date
end
issue.update_attributes(changes) unless changes.empty?
end
def reschedule_dependent_issue(issue = self, options = {}) #start_date_to = nil, due_date_to = nil
cache_change(issue, options)
process_child_issues(issue) if !issue.leaf?
process_following_issues(issue)
update_parent_start_and_due(issue) if issue.parent_id
end
def process_child_issues(issue)
childs_with_nil_start_dates = []
issue.leaves.each do |leaf|
start_date = cached_value(issue, :start_date)
child_start_date = cached_value(leaf, :start_date)
cache_change(issue, :start_date => child_start_date) if start_date.nil?
if child_start_date.nil? or
(start_date > child_start_date) or
(start_date < child_start_date and issue.start_date == leaf.start_date)
reschedule_dependent_issue(leaf, :start_date => start_date)
end
end
end
def process_following_issues(issue)
issue.relations_from.each do |relation|
if is_a_link_with_following_issue?(relation) && due_date = cached_value(issue, :due_date)
new_start_date = RedmineBetterGanttChart::Calendar.workdays_from_date(due_date, relation.delay) + 1.day
new_start_date = RedmineBetterGanttChart::Calendar.next_working_day(new_start_date)
reschedule_dependent_issue(relation.issue_to, :start_date => new_start_date)
end
end
end
def is_a_link_with_following_issue?(relation)
relation.issue_to && relation.relation_type == IssueRelation::TYPE_PRECEDES
end
def reschedule_parents
@parents.each_pair do |parent_id, children|
parent_min_start = min_parent_start(parent_id)
parent_max_start = max_parent_due(parent_id)
cache_change(parent_id, :start_date => parent_min_start,
:due_date => parent_max_start)
children.each do |child| # If parent's start is changing, change start_date of any childs that have empty start_date
if cached_value(child, :start_date).nil?
cache_change(child, :start_date => parent_min_start, :parent => true)
end
end
process_following_issues(Issue.find(parent_id))
end
end
# Caches changes to be applied later. If no attributes to change given, just caches current values.
# Use :parent => true to just change one date without changing the other. If :parent is not specified,
# change of one of the issue dates will cause change of the other.
#
# If no options is provided existing, issue cache is initialized.
def cache_change(issue, options = {})
if issue.is_a?(Integer)
issue_id = issue
issue = Issue.find(issue_id) unless options[:start_date] && options[:due_date] # optimization for the case when issue is not required
else
issue_id = issue.id
end
@changes[issue_id] ||= {}
new_dates = {}
if options.empty? || (options[:start_date] && options[:due_date])
# Both or none dates changed
[:start_date, :due_date].each do |attr|
new_dates[attr] = options[attr] || @changes[issue_id][attr] || issue.send(attr)
end
else
# One of the dates changed - change another accordingly
changed_attr = options[:start_date] && :start_date || :due_date
other_attr = if changed_attr == :start_date then :due_date else :start_date end
new_dates[changed_attr] = options[changed_attr]
if issue.send(other_attr)
if options[:parent]
new_dates[other_attr] = issue.send(other_attr)
else
new_dates[other_attr] = RedmineBetterGanttChart::Calendar.workdays_from_date(issue.send(other_attr), new_dates[changed_attr] - issue.send(changed_attr))
end
end
end
[:start_date, :due_date].each do |attr|
@changes[issue_id][attr] = new_dates[attr].to_date if new_dates[attr]
end
end
# Returns cached value or caches it if it hasn't been cached yet
def cached_value(issue, attr)
issue_id = issue.is_a?(Integer) ? issue : issue.id
cache_change(issue_id) unless @changes[issue_id]
@changes[issue_id][attr] || @changes[issue_id][:start_date]
end
# Each time we update cache of a child issue, need to update cache of the parent issue
# by setting start_date to min(parent.all_children) and due_date to max(parent.all_children).
# Apparently, to do so, first we need to add to cache all child issues of the parent, even if
# they are not affected by rescheduling.
def update_parent_start_and_due(issue)
current_parent_id = issue.parent_id
unless @parents[current_parent_id]
# This parent is touched for the first time, let's cache it's children
@parents[current_parent_id] = [issue.id] # at least the current issue is a child - even if it is not saved yet (is is possible?)
issue.parent.children.each do |child|
cache_change(child) unless @changes[child]
@parents[current_parent_id] << child.id
end
end
end
def min_parent_start(current_parent_id)
@parents[current_parent_id].uniq.inject(Date.new(5000)) do |min, child_id| # Someone needs to update this before 01/01/5000
min = min < (current_child_start = cached_value(child_id, :start_date)) ? min : current_child_start rescue min
end
end
def max_parent_due(current_parent_id)
@parents[current_parent_id].uniq.inject(Date.new) do |max, child_id|
max = max > (current_child_due = cached_value(child_id, :due_date)) ? max : current_child_due rescue max
end
end
# Returns the time scheduled for this issue in working days.
#
def duration_with_work_days
if self.start_date && self.due_date
RedmineBetterGanttChart::Calendar.workdays_between(self.start_date, self.due_date)
else
0
end
end
# Changes behaviour of reschedule_after method
def reschedule_after_with_earlier_date(date)
return if date.nil?
if start_date.blank? || start_date != date
self.start_date = date
if due_date.present?
self.due_date = RedmineBetterGanttChart::Calendar.workdays_from_date(date, duration - 1)
end
save
end
end
# Modifies validation of soonest start date for a new task:
# if parent task has dependency, start date cannot be earlier than start date of the parent.
def soonest_start_with_dependent_parent_validation
@soonest_start ||= (
relations_to.collect{|relation| relation.successor_soonest_start} +
ancestors.collect(&:soonest_start) +
[parent_start_constraint]
).compact.max
end
# Returns [soonest_start_date] if parent task has dependency contstraints
# or [nil] otherwise
def parent_start_constraint
if parent_issue_id && @parent_issue
@parent_issue.soonest_start
else
nil
end
end
end
end
end