##// END OF EJS Templates
Make the 'duplicates of' relation asymmetric:...
Jean-Philippe Lang -
r1474:7042879811ab
parent child
Show More
@@ -1,256 +1,256
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class Issue < ActiveRecord::Base
18 class Issue < ActiveRecord::Base
19 belongs_to :project
19 belongs_to :project
20 belongs_to :tracker
20 belongs_to :tracker
21 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
21 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
22 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
22 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
23 belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id'
23 belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id'
24 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
24 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
25 belongs_to :priority, :class_name => 'Enumeration', :foreign_key => 'priority_id'
25 belongs_to :priority, :class_name => 'Enumeration', :foreign_key => 'priority_id'
26 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
26 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
27
27
28 has_many :journals, :as => :journalized, :dependent => :destroy
28 has_many :journals, :as => :journalized, :dependent => :destroy
29 has_many :attachments, :as => :container, :dependent => :destroy
29 has_many :attachments, :as => :container, :dependent => :destroy
30 has_many :time_entries, :dependent => :delete_all
30 has_many :time_entries, :dependent => :delete_all
31 has_many :custom_values, :dependent => :delete_all, :as => :customized
31 has_many :custom_values, :dependent => :delete_all, :as => :customized
32 has_many :custom_fields, :through => :custom_values
32 has_many :custom_fields, :through => :custom_values
33 has_and_belongs_to_many :changesets, :order => "revision ASC"
33 has_and_belongs_to_many :changesets, :order => "revision ASC"
34
34
35 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
35 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
36 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
36 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
37
37
38 acts_as_watchable
38 acts_as_watchable
39 acts_as_searchable :columns => ['subject', "#{table_name}.description"], :include => :project, :with => {:journal => :issue}
39 acts_as_searchable :columns => ['subject', "#{table_name}.description"], :include => :project, :with => {:journal => :issue}
40 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id}: #{o.subject}"},
40 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id}: #{o.subject}"},
41 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}}
41 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}}
42
42
43 validates_presence_of :subject, :description, :priority, :project, :tracker, :author, :status
43 validates_presence_of :subject, :description, :priority, :project, :tracker, :author, :status
44 validates_length_of :subject, :maximum => 255
44 validates_length_of :subject, :maximum => 255
45 validates_inclusion_of :done_ratio, :in => 0..100
45 validates_inclusion_of :done_ratio, :in => 0..100
46 validates_numericality_of :estimated_hours, :allow_nil => true
46 validates_numericality_of :estimated_hours, :allow_nil => true
47 validates_associated :custom_values, :on => :update
47 validates_associated :custom_values, :on => :update
48
48
49 def after_initialize
49 def after_initialize
50 if new_record?
50 if new_record?
51 # set default values for new records only
51 # set default values for new records only
52 self.status ||= IssueStatus.default
52 self.status ||= IssueStatus.default
53 self.priority ||= Enumeration.default('IPRI')
53 self.priority ||= Enumeration.default('IPRI')
54 end
54 end
55 end
55 end
56
56
57 def copy_from(arg)
57 def copy_from(arg)
58 issue = arg.is_a?(Issue) ? arg : Issue.find(arg)
58 issue = arg.is_a?(Issue) ? arg : Issue.find(arg)
59 self.attributes = issue.attributes.dup
59 self.attributes = issue.attributes.dup
60 self.custom_values = issue.custom_values.collect {|v| v.clone}
60 self.custom_values = issue.custom_values.collect {|v| v.clone}
61 self
61 self
62 end
62 end
63
63
64 # Move an issue to a new project and tracker
64 # Move an issue to a new project and tracker
65 def move_to(new_project, new_tracker = nil)
65 def move_to(new_project, new_tracker = nil)
66 transaction do
66 transaction do
67 if new_project && project_id != new_project.id
67 if new_project && project_id != new_project.id
68 # delete issue relations
68 # delete issue relations
69 unless Setting.cross_project_issue_relations?
69 unless Setting.cross_project_issue_relations?
70 self.relations_from.clear
70 self.relations_from.clear
71 self.relations_to.clear
71 self.relations_to.clear
72 end
72 end
73 # issue is moved to another project
73 # issue is moved to another project
74 self.category = nil
74 self.category = nil
75 self.fixed_version = nil
75 self.fixed_version = nil
76 self.project = new_project
76 self.project = new_project
77 end
77 end
78 if new_tracker
78 if new_tracker
79 self.tracker = new_tracker
79 self.tracker = new_tracker
80 end
80 end
81 if save
81 if save
82 # Manually update project_id on related time entries
82 # Manually update project_id on related time entries
83 TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
83 TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
84 else
84 else
85 rollback_db_transaction
85 rollback_db_transaction
86 return false
86 return false
87 end
87 end
88 end
88 end
89 return true
89 return true
90 end
90 end
91
91
92 def priority_id=(pid)
92 def priority_id=(pid)
93 self.priority = nil
93 self.priority = nil
94 write_attribute(:priority_id, pid)
94 write_attribute(:priority_id, pid)
95 end
95 end
96
96
97 def estimated_hours=(h)
97 def estimated_hours=(h)
98 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
98 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
99 end
99 end
100
100
101 def validate
101 def validate
102 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
102 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
103 errors.add :due_date, :activerecord_error_not_a_date
103 errors.add :due_date, :activerecord_error_not_a_date
104 end
104 end
105
105
106 if self.due_date and self.start_date and self.due_date < self.start_date
106 if self.due_date and self.start_date and self.due_date < self.start_date
107 errors.add :due_date, :activerecord_error_greater_than_start_date
107 errors.add :due_date, :activerecord_error_greater_than_start_date
108 end
108 end
109
109
110 if start_date && soonest_start && start_date < soonest_start
110 if start_date && soonest_start && start_date < soonest_start
111 errors.add :start_date, :activerecord_error_invalid
111 errors.add :start_date, :activerecord_error_invalid
112 end
112 end
113 end
113 end
114
114
115 def validate_on_create
115 def validate_on_create
116 errors.add :tracker_id, :activerecord_error_invalid unless project.trackers.include?(tracker)
116 errors.add :tracker_id, :activerecord_error_invalid unless project.trackers.include?(tracker)
117 end
117 end
118
118
119 def before_create
119 def before_create
120 # default assignment based on category
120 # default assignment based on category
121 if assigned_to.nil? && category && category.assigned_to
121 if assigned_to.nil? && category && category.assigned_to
122 self.assigned_to = category.assigned_to
122 self.assigned_to = category.assigned_to
123 end
123 end
124 end
124 end
125
125
126 def before_save
126 def before_save
127 if @current_journal
127 if @current_journal
128 # attributes changes
128 # attributes changes
129 (Issue.column_names - %w(id description)).each {|c|
129 (Issue.column_names - %w(id description)).each {|c|
130 @current_journal.details << JournalDetail.new(:property => 'attr',
130 @current_journal.details << JournalDetail.new(:property => 'attr',
131 :prop_key => c,
131 :prop_key => c,
132 :old_value => @issue_before_change.send(c),
132 :old_value => @issue_before_change.send(c),
133 :value => send(c)) unless send(c)==@issue_before_change.send(c)
133 :value => send(c)) unless send(c)==@issue_before_change.send(c)
134 }
134 }
135 # custom fields changes
135 # custom fields changes
136 custom_values.each {|c|
136 custom_values.each {|c|
137 next if (@custom_values_before_change[c.custom_field_id]==c.value ||
137 next if (@custom_values_before_change[c.custom_field_id]==c.value ||
138 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
138 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
139 @current_journal.details << JournalDetail.new(:property => 'cf',
139 @current_journal.details << JournalDetail.new(:property => 'cf',
140 :prop_key => c.custom_field_id,
140 :prop_key => c.custom_field_id,
141 :old_value => @custom_values_before_change[c.custom_field_id],
141 :old_value => @custom_values_before_change[c.custom_field_id],
142 :value => c.value)
142 :value => c.value)
143 }
143 }
144 @current_journal.save
144 @current_journal.save
145 end
145 end
146 # Save the issue even if the journal is not saved (because empty)
146 # Save the issue even if the journal is not saved (because empty)
147 true
147 true
148 end
148 end
149
149
150 def after_save
150 def after_save
151 # Reload is needed in order to get the right status
151 # Reload is needed in order to get the right status
152 reload
152 reload
153
153
154 # Update start/due dates of following issues
154 # Update start/due dates of following issues
155 relations_from.each(&:set_issue_to_dates)
155 relations_from.each(&:set_issue_to_dates)
156
156
157 # Close duplicates if the issue was closed
157 # Close duplicates if the issue was closed
158 if @issue_before_change && !@issue_before_change.closed? && self.closed?
158 if @issue_before_change && !@issue_before_change.closed? && self.closed?
159 duplicates.each do |duplicate|
159 duplicates.each do |duplicate|
160 # Reload is need in case the duplicate was updated by a previous duplicate
160 # Reload is need in case the duplicate was updated by a previous duplicate
161 duplicate.reload
161 duplicate.reload
162 # Don't re-close it if it's already closed
162 # Don't re-close it if it's already closed
163 next if duplicate.closed?
163 next if duplicate.closed?
164 # Same user and notes
164 # Same user and notes
165 duplicate.init_journal(@current_journal.user, @current_journal.notes)
165 duplicate.init_journal(@current_journal.user, @current_journal.notes)
166 duplicate.update_attribute :status, self.status
166 duplicate.update_attribute :status, self.status
167 end
167 end
168 end
168 end
169 end
169 end
170
170
171 def custom_value_for(custom_field)
171 def custom_value_for(custom_field)
172 self.custom_values.each {|v| return v if v.custom_field_id == custom_field.id }
172 self.custom_values.each {|v| return v if v.custom_field_id == custom_field.id }
173 return nil
173 return nil
174 end
174 end
175
175
176 def init_journal(user, notes = "")
176 def init_journal(user, notes = "")
177 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
177 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
178 @issue_before_change = self.clone
178 @issue_before_change = self.clone
179 @issue_before_change.status = self.status
179 @issue_before_change.status = self.status
180 @custom_values_before_change = {}
180 @custom_values_before_change = {}
181 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
181 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
182 @current_journal
182 @current_journal
183 end
183 end
184
184
185 # Return true if the issue is closed, otherwise false
185 # Return true if the issue is closed, otherwise false
186 def closed?
186 def closed?
187 self.status.is_closed?
187 self.status.is_closed?
188 end
188 end
189
189
190 # Users the issue can be assigned to
190 # Users the issue can be assigned to
191 def assignable_users
191 def assignable_users
192 project.assignable_users
192 project.assignable_users
193 end
193 end
194
194
195 # Returns an array of status that user is able to apply
195 # Returns an array of status that user is able to apply
196 def new_statuses_allowed_to(user)
196 def new_statuses_allowed_to(user)
197 statuses = status.find_new_statuses_allowed_to(user.role_for_project(project), tracker)
197 statuses = status.find_new_statuses_allowed_to(user.role_for_project(project), tracker)
198 statuses << status unless statuses.empty?
198 statuses << status unless statuses.empty?
199 statuses.uniq.sort
199 statuses.uniq.sort
200 end
200 end
201
201
202 # Returns the mail adresses of users that should be notified for the issue
202 # Returns the mail adresses of users that should be notified for the issue
203 def recipients
203 def recipients
204 recipients = project.recipients
204 recipients = project.recipients
205 # Author and assignee are always notified unless they have been locked
205 # Author and assignee are always notified unless they have been locked
206 recipients << author.mail if author && author.active?
206 recipients << author.mail if author && author.active?
207 recipients << assigned_to.mail if assigned_to && assigned_to.active?
207 recipients << assigned_to.mail if assigned_to && assigned_to.active?
208 recipients.compact.uniq
208 recipients.compact.uniq
209 end
209 end
210
210
211 def spent_hours
211 def spent_hours
212 @spent_hours ||= time_entries.sum(:hours) || 0
212 @spent_hours ||= time_entries.sum(:hours) || 0
213 end
213 end
214
214
215 def relations
215 def relations
216 (relations_from + relations_to).sort
216 (relations_from + relations_to).sort
217 end
217 end
218
218
219 def all_dependent_issues
219 def all_dependent_issues
220 dependencies = []
220 dependencies = []
221 relations_from.each do |relation|
221 relations_from.each do |relation|
222 dependencies << relation.issue_to
222 dependencies << relation.issue_to
223 dependencies += relation.issue_to.all_dependent_issues
223 dependencies += relation.issue_to.all_dependent_issues
224 end
224 end
225 dependencies
225 dependencies
226 end
226 end
227
227
228 # Returns an array of the duplicate issues
228 # Returns an array of issues that duplicate this one
229 def duplicates
229 def duplicates
230 relations.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.other_issue(self)}
230 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
231 end
231 end
232
232
233 # Returns the due date or the target due date if any
233 # Returns the due date or the target due date if any
234 # Used on gantt chart
234 # Used on gantt chart
235 def due_before
235 def due_before
236 due_date || (fixed_version ? fixed_version.effective_date : nil)
236 due_date || (fixed_version ? fixed_version.effective_date : nil)
237 end
237 end
238
238
239 def duration
239 def duration
240 (start_date && due_date) ? due_date - start_date : 0
240 (start_date && due_date) ? due_date - start_date : 0
241 end
241 end
242
242
243 def soonest_start
243 def soonest_start
244 @soonest_start ||= relations_to.collect{|relation| relation.successor_soonest_start}.compact.min
244 @soonest_start ||= relations_to.collect{|relation| relation.successor_soonest_start}.compact.min
245 end
245 end
246
246
247 def self.visible_by(usr)
247 def self.visible_by(usr)
248 with_scope(:find => { :conditions => Project.visible_by(usr) }) do
248 with_scope(:find => { :conditions => Project.visible_by(usr) }) do
249 yield
249 yield
250 end
250 end
251 end
251 end
252
252
253 def to_s
253 def to_s
254 "#{tracker} ##{id}: #{subject}"
254 "#{tracker} ##{id}: #{subject}"
255 end
255 end
256 end
256 end
@@ -1,79 +1,79
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class IssueRelation < ActiveRecord::Base
18 class IssueRelation < ActiveRecord::Base
19 belongs_to :issue_from, :class_name => 'Issue', :foreign_key => 'issue_from_id'
19 belongs_to :issue_from, :class_name => 'Issue', :foreign_key => 'issue_from_id'
20 belongs_to :issue_to, :class_name => 'Issue', :foreign_key => 'issue_to_id'
20 belongs_to :issue_to, :class_name => 'Issue', :foreign_key => 'issue_to_id'
21
21
22 TYPE_RELATES = "relates"
22 TYPE_RELATES = "relates"
23 TYPE_DUPLICATES = "duplicates"
23 TYPE_DUPLICATES = "duplicates"
24 TYPE_BLOCKS = "blocks"
24 TYPE_BLOCKS = "blocks"
25 TYPE_PRECEDES = "precedes"
25 TYPE_PRECEDES = "precedes"
26
26
27 TYPES = { TYPE_RELATES => { :name => :label_relates_to, :sym_name => :label_relates_to, :order => 1 },
27 TYPES = { TYPE_RELATES => { :name => :label_relates_to, :sym_name => :label_relates_to, :order => 1 },
28 TYPE_DUPLICATES => { :name => :label_duplicates, :sym_name => :label_duplicates, :order => 2 },
28 TYPE_DUPLICATES => { :name => :label_duplicates, :sym_name => :label_duplicated_by, :order => 2 },
29 TYPE_BLOCKS => { :name => :label_blocks, :sym_name => :label_blocked_by, :order => 3 },
29 TYPE_BLOCKS => { :name => :label_blocks, :sym_name => :label_blocked_by, :order => 3 },
30 TYPE_PRECEDES => { :name => :label_precedes, :sym_name => :label_follows, :order => 4 },
30 TYPE_PRECEDES => { :name => :label_precedes, :sym_name => :label_follows, :order => 4 },
31 }.freeze
31 }.freeze
32
32
33 validates_presence_of :issue_from, :issue_to, :relation_type
33 validates_presence_of :issue_from, :issue_to, :relation_type
34 validates_inclusion_of :relation_type, :in => TYPES.keys
34 validates_inclusion_of :relation_type, :in => TYPES.keys
35 validates_numericality_of :delay, :allow_nil => true
35 validates_numericality_of :delay, :allow_nil => true
36 validates_uniqueness_of :issue_to_id, :scope => :issue_from_id
36 validates_uniqueness_of :issue_to_id, :scope => :issue_from_id
37
37
38 def validate
38 def validate
39 if issue_from && issue_to
39 if issue_from && issue_to
40 errors.add :issue_to_id, :activerecord_error_invalid if issue_from_id == issue_to_id
40 errors.add :issue_to_id, :activerecord_error_invalid if issue_from_id == issue_to_id
41 errors.add :issue_to_id, :activerecord_error_not_same_project unless issue_from.project_id == issue_to.project_id || Setting.cross_project_issue_relations?
41 errors.add :issue_to_id, :activerecord_error_not_same_project unless issue_from.project_id == issue_to.project_id || Setting.cross_project_issue_relations?
42 errors.add_to_base :activerecord_error_circular_dependency if issue_to.all_dependent_issues.include? issue_from
42 errors.add_to_base :activerecord_error_circular_dependency if issue_to.all_dependent_issues.include? issue_from
43 end
43 end
44 end
44 end
45
45
46 def other_issue(issue)
46 def other_issue(issue)
47 (self.issue_from_id == issue.id) ? issue_to : issue_from
47 (self.issue_from_id == issue.id) ? issue_to : issue_from
48 end
48 end
49
49
50 def label_for(issue)
50 def label_for(issue)
51 TYPES[relation_type] ? TYPES[relation_type][(self.issue_from_id == issue.id) ? :name : :sym_name] : :unknow
51 TYPES[relation_type] ? TYPES[relation_type][(self.issue_from_id == issue.id) ? :name : :sym_name] : :unknow
52 end
52 end
53
53
54 def before_save
54 def before_save
55 if TYPE_PRECEDES == relation_type
55 if TYPE_PRECEDES == relation_type
56 self.delay ||= 0
56 self.delay ||= 0
57 else
57 else
58 self.delay = nil
58 self.delay = nil
59 end
59 end
60 set_issue_to_dates
60 set_issue_to_dates
61 end
61 end
62
62
63 def set_issue_to_dates
63 def set_issue_to_dates
64 soonest_start = self.successor_soonest_start
64 soonest_start = self.successor_soonest_start
65 if soonest_start && (!issue_to.start_date || issue_to.start_date < soonest_start)
65 if soonest_start && (!issue_to.start_date || issue_to.start_date < soonest_start)
66 issue_to.start_date, issue_to.due_date = successor_soonest_start, successor_soonest_start + issue_to.duration
66 issue_to.start_date, issue_to.due_date = successor_soonest_start, successor_soonest_start + issue_to.duration
67 issue_to.save
67 issue_to.save
68 end
68 end
69 end
69 end
70
70
71 def successor_soonest_start
71 def successor_soonest_start
72 return nil unless (TYPE_PRECEDES == self.relation_type) && (issue_from.start_date || issue_from.due_date)
72 return nil unless (TYPE_PRECEDES == self.relation_type) && (issue_from.start_date || issue_from.due_date)
73 (issue_from.due_date || issue_from.start_date) + 1 + delay
73 (issue_from.due_date || issue_from.start_date) + 1 + delay
74 end
74 end
75
75
76 def <=>(relation)
76 def <=>(relation)
77 TYPES[self.relation_type][:order] <=> TYPES[relation.relation_type][:order]
77 TYPES[self.relation_type][:order] <=> TYPES[relation.relation_type][:order]
78 end
78 end
79 end
79 end
@@ -1,88 +1,108
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.dirname(__FILE__) + '/../test_helper'
18 require File.dirname(__FILE__) + '/../test_helper'
19
19
20 class IssueTest < Test::Unit::TestCase
20 class IssueTest < Test::Unit::TestCase
21 fixtures :projects, :users, :members, :trackers, :projects_trackers, :issue_statuses, :issue_categories, :enumerations, :issues, :custom_fields, :custom_values, :time_entries
21 fixtures :projects, :users, :members, :trackers, :projects_trackers, :issue_statuses, :issue_categories, :enumerations, :issues, :custom_fields, :custom_values, :time_entries
22
22
23 def test_create
23 def test_create
24 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => Enumeration.get_values('IPRI').first, :subject => 'test_create', :description => 'IssueTest#test_create', :estimated_hours => '1:30')
24 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => Enumeration.get_values('IPRI').first, :subject => 'test_create', :description => 'IssueTest#test_create', :estimated_hours => '1:30')
25 assert issue.save
25 assert issue.save
26 issue.reload
26 issue.reload
27 assert_equal 1.5, issue.estimated_hours
27 assert_equal 1.5, issue.estimated_hours
28 end
28 end
29
29
30 def test_category_based_assignment
30 def test_category_based_assignment
31 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => Enumeration.get_values('IPRI').first, :subject => 'Assignment test', :description => 'Assignment test', :category_id => 1)
31 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => Enumeration.get_values('IPRI').first, :subject => 'Assignment test', :description => 'Assignment test', :category_id => 1)
32 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
32 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
33 end
33 end
34
34
35 def test_copy
35 def test_copy
36 issue = Issue.new.copy_from(1)
36 issue = Issue.new.copy_from(1)
37 assert issue.save
37 assert issue.save
38 issue.reload
38 issue.reload
39 orig = Issue.find(1)
39 orig = Issue.find(1)
40 assert_equal orig.subject, issue.subject
40 assert_equal orig.subject, issue.subject
41 assert_equal orig.tracker, issue.tracker
41 assert_equal orig.tracker, issue.tracker
42 assert_equal orig.custom_values.first.value, issue.custom_values.first.value
42 assert_equal orig.custom_values.first.value, issue.custom_values.first.value
43 end
43 end
44
44
45 def test_close_duplicates
45 def test_should_close_duplicates
46 # Create 3 issues
46 # Create 3 issues
47 issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => Enumeration.get_values('IPRI').first, :subject => 'Duplicates test', :description => 'Duplicates test')
47 issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => Enumeration.get_values('IPRI').first, :subject => 'Duplicates test', :description => 'Duplicates test')
48 assert issue1.save
48 assert issue1.save
49 issue2 = issue1.clone
49 issue2 = issue1.clone
50 assert issue2.save
50 assert issue2.save
51 issue3 = issue1.clone
51 issue3 = issue1.clone
52 assert issue3.save
52 assert issue3.save
53
53
54 # 2 is a dupe of 1
54 # 2 is a dupe of 1
55 IssueRelation.create(:issue_from => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_DUPLICATES)
55 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
56 # And 3 is a dupe of 2
56 # And 3 is a dupe of 2
57 IssueRelation.create(:issue_from => issue2, :issue_to => issue3, :relation_type => IssueRelation::TYPE_DUPLICATES)
57 IssueRelation.create(:issue_from => issue3, :issue_to => issue2, :relation_type => IssueRelation::TYPE_DUPLICATES)
58 # And 3 is a dupe of 1 (circular duplicates)
58 # And 3 is a dupe of 1 (circular duplicates)
59 IssueRelation.create(:issue_from => issue1, :issue_to => issue3, :relation_type => IssueRelation::TYPE_DUPLICATES)
59 IssueRelation.create(:issue_from => issue3, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
60
60
61 assert issue1.reload.duplicates.include?(issue2)
61 assert issue1.reload.duplicates.include?(issue2)
62
62
63 # Closing issue 1
63 # Closing issue 1
64 issue1.init_journal(User.find(:first), "Closing issue1")
64 issue1.init_journal(User.find(:first), "Closing issue1")
65 issue1.status = IssueStatus.find :first, :conditions => {:is_closed => true}
65 issue1.status = IssueStatus.find :first, :conditions => {:is_closed => true}
66 assert issue1.save
66 assert issue1.save
67 # 2 and 3 should be also closed
67 # 2 and 3 should be also closed
68 assert issue2.reload.closed?
68 assert issue2.reload.closed?
69 assert issue3.reload.closed?
69 assert issue3.reload.closed?
70 end
70 end
71
71
72 def test_should_not_close_duplicated_issue
73 # Create 3 issues
74 issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => Enumeration.get_values('IPRI').first, :subject => 'Duplicates test', :description => 'Duplicates test')
75 assert issue1.save
76 issue2 = issue1.clone
77 assert issue2.save
78
79 # 2 is a dupe of 1
80 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
81 # 2 is a dup of 1 but 1 is not a duplicate of 2
82 assert !issue2.reload.duplicates.include?(issue1)
83
84 # Closing issue 2
85 issue2.init_journal(User.find(:first), "Closing issue2")
86 issue2.status = IssueStatus.find :first, :conditions => {:is_closed => true}
87 assert issue2.save
88 # 1 should not be also closed
89 assert !issue1.reload.closed?
90 end
91
72 def test_move_to_another_project
92 def test_move_to_another_project
73 issue = Issue.find(1)
93 issue = Issue.find(1)
74 assert issue.move_to(Project.find(2))
94 assert issue.move_to(Project.find(2))
75 issue.reload
95 issue.reload
76 assert_equal 2, issue.project_id
96 assert_equal 2, issue.project_id
77 # Category removed
97 # Category removed
78 assert_nil issue.category
98 assert_nil issue.category
79 # Make sure time entries were move to the target project
99 # Make sure time entries were move to the target project
80 assert_equal 2, issue.time_entries.first.project_id
100 assert_equal 2, issue.time_entries.first.project_id
81 end
101 end
82
102
83 def test_issue_destroy
103 def test_issue_destroy
84 Issue.find(1).destroy
104 Issue.find(1).destroy
85 assert_nil Issue.find_by_id(1)
105 assert_nil Issue.find_by_id(1)
86 assert_nil TimeEntry.find_by_issue_id(1)
106 assert_nil TimeEntry.find_by_issue_id(1)
87 end
107 end
88 end
108 end
General Comments 0
You need to be logged in to leave comments. Login now