##// END OF EJS Templates
Copy issue status on project copy (#3877)....
Jean-Philippe Lang -
r2961:3fc655904f90
parent child
Show More
@@ -1,347 +1,348
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 => 'IssuePriority', :foreign_key => 'priority_id'
25 belongs_to :priority, :class_name => 'IssuePriority', :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 :time_entries, :dependent => :delete_all
29 has_many :time_entries, :dependent => :delete_all
30 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
30 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
31
31
32 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
32 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
33 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
33 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
34
34
35 acts_as_attachable :after_remove => :attachment_removed
35 acts_as_attachable :after_remove => :attachment_removed
36 acts_as_customizable
36 acts_as_customizable
37 acts_as_watchable
37 acts_as_watchable
38 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
38 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
39 :include => [:project, :journals],
39 :include => [:project, :journals],
40 # sort by id so that limited eager loading doesn't break with postgresql
40 # sort by id so that limited eager loading doesn't break with postgresql
41 :order_column => "#{table_name}.id"
41 :order_column => "#{table_name}.id"
42 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
42 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
43 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
43 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
44 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
44 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
45
45
46 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
46 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
47 :author_key => :author_id
47 :author_key => :author_id
48
48
49 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
49 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
50 validates_length_of :subject, :maximum => 255
50 validates_length_of :subject, :maximum => 255
51 validates_inclusion_of :done_ratio, :in => 0..100
51 validates_inclusion_of :done_ratio, :in => 0..100
52 validates_numericality_of :estimated_hours, :allow_nil => true
52 validates_numericality_of :estimated_hours, :allow_nil => true
53
53
54 named_scope :visible, lambda {|*args| { :include => :project,
54 named_scope :visible, lambda {|*args| { :include => :project,
55 :conditions => Project.allowed_to_condition(args.first || User.current, :view_issues) } }
55 :conditions => Project.allowed_to_condition(args.first || User.current, :view_issues) } }
56
56
57 named_scope :open, :conditions => ["#{IssueStatus.table_name}.is_closed = ?", false], :include => :status
57 named_scope :open, :conditions => ["#{IssueStatus.table_name}.is_closed = ?", false], :include => :status
58
58
59 after_save :create_journal
59 after_save :create_journal
60
60
61 # Returns true if usr or current user is allowed to view the issue
61 # Returns true if usr or current user is allowed to view the issue
62 def visible?(usr=nil)
62 def visible?(usr=nil)
63 (usr || User.current).allowed_to?(:view_issues, self.project)
63 (usr || User.current).allowed_to?(:view_issues, self.project)
64 end
64 end
65
65
66 def after_initialize
66 def after_initialize
67 if new_record?
67 if new_record?
68 # set default values for new records only
68 # set default values for new records only
69 self.status ||= IssueStatus.default
69 self.status ||= IssueStatus.default
70 self.priority ||= IssuePriority.default
70 self.priority ||= IssuePriority.default
71 end
71 end
72 end
72 end
73
73
74 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
74 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
75 def available_custom_fields
75 def available_custom_fields
76 (project && tracker) ? project.all_issue_custom_fields.select {|c| tracker.custom_fields.include? c } : []
76 (project && tracker) ? project.all_issue_custom_fields.select {|c| tracker.custom_fields.include? c } : []
77 end
77 end
78
78
79 def copy_from(arg)
79 def copy_from(arg)
80 issue = arg.is_a?(Issue) ? arg : Issue.find(arg)
80 issue = arg.is_a?(Issue) ? arg : Issue.find(arg)
81 self.attributes = issue.attributes.dup.except("id", "created_on", "updated_on")
81 self.attributes = issue.attributes.dup.except("id", "created_on", "updated_on")
82 self.custom_values = issue.custom_values.collect {|v| v.clone}
82 self.custom_values = issue.custom_values.collect {|v| v.clone}
83 self.status = issue.status
83 self
84 self
84 end
85 end
85
86
86 # Moves/copies an issue to a new project and tracker
87 # Moves/copies an issue to a new project and tracker
87 # Returns the moved/copied issue on success, false on failure
88 # Returns the moved/copied issue on success, false on failure
88 def move_to(new_project, new_tracker = nil, options = {})
89 def move_to(new_project, new_tracker = nil, options = {})
89 options ||= {}
90 options ||= {}
90 issue = options[:copy] ? self.clone : self
91 issue = options[:copy] ? self.clone : self
91 transaction do
92 transaction do
92 if new_project && issue.project_id != new_project.id
93 if new_project && issue.project_id != new_project.id
93 # delete issue relations
94 # delete issue relations
94 unless Setting.cross_project_issue_relations?
95 unless Setting.cross_project_issue_relations?
95 issue.relations_from.clear
96 issue.relations_from.clear
96 issue.relations_to.clear
97 issue.relations_to.clear
97 end
98 end
98 # issue is moved to another project
99 # issue is moved to another project
99 # reassign to the category with same name if any
100 # reassign to the category with same name if any
100 new_category = issue.category.nil? ? nil : new_project.issue_categories.find_by_name(issue.category.name)
101 new_category = issue.category.nil? ? nil : new_project.issue_categories.find_by_name(issue.category.name)
101 issue.category = new_category
102 issue.category = new_category
102 issue.fixed_version = nil
103 issue.fixed_version = nil
103 issue.project = new_project
104 issue.project = new_project
104 end
105 end
105 if new_tracker
106 if new_tracker
106 issue.tracker = new_tracker
107 issue.tracker = new_tracker
107 end
108 end
108 if options[:copy]
109 if options[:copy]
109 issue.custom_field_values = self.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
110 issue.custom_field_values = self.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
110 issue.status = self.status
111 issue.status = self.status
111 end
112 end
112 if issue.save
113 if issue.save
113 unless options[:copy]
114 unless options[:copy]
114 # Manually update project_id on related time entries
115 # Manually update project_id on related time entries
115 TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
116 TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
116 end
117 end
117 else
118 else
118 Issue.connection.rollback_db_transaction
119 Issue.connection.rollback_db_transaction
119 return false
120 return false
120 end
121 end
121 end
122 end
122 return issue
123 return issue
123 end
124 end
124
125
125 def priority_id=(pid)
126 def priority_id=(pid)
126 self.priority = nil
127 self.priority = nil
127 write_attribute(:priority_id, pid)
128 write_attribute(:priority_id, pid)
128 end
129 end
129
130
130 def estimated_hours=(h)
131 def estimated_hours=(h)
131 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
132 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
132 end
133 end
133
134
134 def validate
135 def validate
135 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
136 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
136 errors.add :due_date, :not_a_date
137 errors.add :due_date, :not_a_date
137 end
138 end
138
139
139 if self.due_date and self.start_date and self.due_date < self.start_date
140 if self.due_date and self.start_date and self.due_date < self.start_date
140 errors.add :due_date, :greater_than_start_date
141 errors.add :due_date, :greater_than_start_date
141 end
142 end
142
143
143 if start_date && soonest_start && start_date < soonest_start
144 if start_date && soonest_start && start_date < soonest_start
144 errors.add :start_date, :invalid
145 errors.add :start_date, :invalid
145 end
146 end
146
147
147 if fixed_version
148 if fixed_version
148 if !assignable_versions.include?(fixed_version)
149 if !assignable_versions.include?(fixed_version)
149 errors.add :fixed_version_id, :inclusion
150 errors.add :fixed_version_id, :inclusion
150 elsif reopened? && fixed_version.closed?
151 elsif reopened? && fixed_version.closed?
151 errors.add_to_base I18n.t(:error_can_not_reopen_issue_on_closed_version)
152 errors.add_to_base I18n.t(:error_can_not_reopen_issue_on_closed_version)
152 end
153 end
153 end
154 end
154 end
155 end
155
156
156 def validate_on_create
157 def validate_on_create
157 errors.add :tracker_id, :invalid unless project.trackers.include?(tracker)
158 errors.add :tracker_id, :invalid unless project.trackers.include?(tracker)
158 end
159 end
159
160
160 def before_create
161 def before_create
161 # default assignment based on category
162 # default assignment based on category
162 if assigned_to.nil? && category && category.assigned_to
163 if assigned_to.nil? && category && category.assigned_to
163 self.assigned_to = category.assigned_to
164 self.assigned_to = category.assigned_to
164 end
165 end
165 end
166 end
166
167
167 def after_save
168 def after_save
168 # Reload is needed in order to get the right status
169 # Reload is needed in order to get the right status
169 reload
170 reload
170
171
171 # Update start/due dates of following issues
172 # Update start/due dates of following issues
172 relations_from.each(&:set_issue_to_dates)
173 relations_from.each(&:set_issue_to_dates)
173
174
174 # Close duplicates if the issue was closed
175 # Close duplicates if the issue was closed
175 if @issue_before_change && !@issue_before_change.closed? && self.closed?
176 if @issue_before_change && !@issue_before_change.closed? && self.closed?
176 duplicates.each do |duplicate|
177 duplicates.each do |duplicate|
177 # Reload is need in case the duplicate was updated by a previous duplicate
178 # Reload is need in case the duplicate was updated by a previous duplicate
178 duplicate.reload
179 duplicate.reload
179 # Don't re-close it if it's already closed
180 # Don't re-close it if it's already closed
180 next if duplicate.closed?
181 next if duplicate.closed?
181 # Same user and notes
182 # Same user and notes
182 duplicate.init_journal(@current_journal.user, @current_journal.notes)
183 duplicate.init_journal(@current_journal.user, @current_journal.notes)
183 duplicate.update_attribute :status, self.status
184 duplicate.update_attribute :status, self.status
184 end
185 end
185 end
186 end
186 end
187 end
187
188
188 def init_journal(user, notes = "")
189 def init_journal(user, notes = "")
189 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
190 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
190 @issue_before_change = self.clone
191 @issue_before_change = self.clone
191 @issue_before_change.status = self.status
192 @issue_before_change.status = self.status
192 @custom_values_before_change = {}
193 @custom_values_before_change = {}
193 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
194 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
194 # Make sure updated_on is updated when adding a note.
195 # Make sure updated_on is updated when adding a note.
195 updated_on_will_change!
196 updated_on_will_change!
196 @current_journal
197 @current_journal
197 end
198 end
198
199
199 # Return true if the issue is closed, otherwise false
200 # Return true if the issue is closed, otherwise false
200 def closed?
201 def closed?
201 self.status.is_closed?
202 self.status.is_closed?
202 end
203 end
203
204
204 # Return true if the issue is being reopened
205 # Return true if the issue is being reopened
205 def reopened?
206 def reopened?
206 if !new_record? && status_id_changed?
207 if !new_record? && status_id_changed?
207 status_was = IssueStatus.find_by_id(status_id_was)
208 status_was = IssueStatus.find_by_id(status_id_was)
208 status_new = IssueStatus.find_by_id(status_id)
209 status_new = IssueStatus.find_by_id(status_id)
209 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
210 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
210 return true
211 return true
211 end
212 end
212 end
213 end
213 false
214 false
214 end
215 end
215
216
216 # Returns true if the issue is overdue
217 # Returns true if the issue is overdue
217 def overdue?
218 def overdue?
218 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
219 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
219 end
220 end
220
221
221 # Users the issue can be assigned to
222 # Users the issue can be assigned to
222 def assignable_users
223 def assignable_users
223 project.assignable_users
224 project.assignable_users
224 end
225 end
225
226
226 # Versions that the issue can be assigned to
227 # Versions that the issue can be assigned to
227 def assignable_versions
228 def assignable_versions
228 @assignable_versions ||= (project.versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort
229 @assignable_versions ||= (project.versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort
229 end
230 end
230
231
231 # Returns true if this issue is blocked by another issue that is still open
232 # Returns true if this issue is blocked by another issue that is still open
232 def blocked?
233 def blocked?
233 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
234 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
234 end
235 end
235
236
236 # Returns an array of status that user is able to apply
237 # Returns an array of status that user is able to apply
237 def new_statuses_allowed_to(user)
238 def new_statuses_allowed_to(user)
238 statuses = status.find_new_statuses_allowed_to(user.roles_for_project(project), tracker)
239 statuses = status.find_new_statuses_allowed_to(user.roles_for_project(project), tracker)
239 statuses << status unless statuses.empty?
240 statuses << status unless statuses.empty?
240 statuses = statuses.uniq.sort
241 statuses = statuses.uniq.sort
241 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
242 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
242 end
243 end
243
244
244 # Returns the mail adresses of users that should be notified for the issue
245 # Returns the mail adresses of users that should be notified for the issue
245 def recipients
246 def recipients
246 recipients = project.recipients
247 recipients = project.recipients
247 # Author and assignee are always notified unless they have been locked
248 # Author and assignee are always notified unless they have been locked
248 recipients << author.mail if author && author.active?
249 recipients << author.mail if author && author.active?
249 recipients << assigned_to.mail if assigned_to && assigned_to.active?
250 recipients << assigned_to.mail if assigned_to && assigned_to.active?
250 recipients.compact.uniq
251 recipients.compact.uniq
251 end
252 end
252
253
253 # Returns the total number of hours spent on this issue.
254 # Returns the total number of hours spent on this issue.
254 #
255 #
255 # Example:
256 # Example:
256 # spent_hours => 0
257 # spent_hours => 0
257 # spent_hours => 50
258 # spent_hours => 50
258 def spent_hours
259 def spent_hours
259 @spent_hours ||= time_entries.sum(:hours) || 0
260 @spent_hours ||= time_entries.sum(:hours) || 0
260 end
261 end
261
262
262 def relations
263 def relations
263 (relations_from + relations_to).sort
264 (relations_from + relations_to).sort
264 end
265 end
265
266
266 def all_dependent_issues
267 def all_dependent_issues
267 dependencies = []
268 dependencies = []
268 relations_from.each do |relation|
269 relations_from.each do |relation|
269 dependencies << relation.issue_to
270 dependencies << relation.issue_to
270 dependencies += relation.issue_to.all_dependent_issues
271 dependencies += relation.issue_to.all_dependent_issues
271 end
272 end
272 dependencies
273 dependencies
273 end
274 end
274
275
275 # Returns an array of issues that duplicate this one
276 # Returns an array of issues that duplicate this one
276 def duplicates
277 def duplicates
277 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
278 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
278 end
279 end
279
280
280 # Returns the due date or the target due date if any
281 # Returns the due date or the target due date if any
281 # Used on gantt chart
282 # Used on gantt chart
282 def due_before
283 def due_before
283 due_date || (fixed_version ? fixed_version.effective_date : nil)
284 due_date || (fixed_version ? fixed_version.effective_date : nil)
284 end
285 end
285
286
286 # Returns the time scheduled for this issue.
287 # Returns the time scheduled for this issue.
287 #
288 #
288 # Example:
289 # Example:
289 # Start Date: 2/26/09, End Date: 3/04/09
290 # Start Date: 2/26/09, End Date: 3/04/09
290 # duration => 6
291 # duration => 6
291 def duration
292 def duration
292 (start_date && due_date) ? due_date - start_date : 0
293 (start_date && due_date) ? due_date - start_date : 0
293 end
294 end
294
295
295 def soonest_start
296 def soonest_start
296 @soonest_start ||= relations_to.collect{|relation| relation.successor_soonest_start}.compact.min
297 @soonest_start ||= relations_to.collect{|relation| relation.successor_soonest_start}.compact.min
297 end
298 end
298
299
299 def to_s
300 def to_s
300 "#{tracker} ##{id}: #{subject}"
301 "#{tracker} ##{id}: #{subject}"
301 end
302 end
302
303
303 # Returns a string of css classes that apply to the issue
304 # Returns a string of css classes that apply to the issue
304 def css_classes
305 def css_classes
305 s = "issue status-#{status.position} priority-#{priority.position}"
306 s = "issue status-#{status.position} priority-#{priority.position}"
306 s << ' closed' if closed?
307 s << ' closed' if closed?
307 s << ' overdue' if overdue?
308 s << ' overdue' if overdue?
308 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
309 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
309 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
310 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
310 s
311 s
311 end
312 end
312
313
313 private
314 private
314
315
315 # Callback on attachment deletion
316 # Callback on attachment deletion
316 def attachment_removed(obj)
317 def attachment_removed(obj)
317 journal = init_journal(User.current)
318 journal = init_journal(User.current)
318 journal.details << JournalDetail.new(:property => 'attachment',
319 journal.details << JournalDetail.new(:property => 'attachment',
319 :prop_key => obj.id,
320 :prop_key => obj.id,
320 :old_value => obj.filename)
321 :old_value => obj.filename)
321 journal.save
322 journal.save
322 end
323 end
323
324
324 # Saves the changes in a Journal
325 # Saves the changes in a Journal
325 # Called after_save
326 # Called after_save
326 def create_journal
327 def create_journal
327 if @current_journal
328 if @current_journal
328 # attributes changes
329 # attributes changes
329 (Issue.column_names - %w(id description lock_version created_on updated_on)).each {|c|
330 (Issue.column_names - %w(id description lock_version created_on updated_on)).each {|c|
330 @current_journal.details << JournalDetail.new(:property => 'attr',
331 @current_journal.details << JournalDetail.new(:property => 'attr',
331 :prop_key => c,
332 :prop_key => c,
332 :old_value => @issue_before_change.send(c),
333 :old_value => @issue_before_change.send(c),
333 :value => send(c)) unless send(c)==@issue_before_change.send(c)
334 :value => send(c)) unless send(c)==@issue_before_change.send(c)
334 }
335 }
335 # custom fields changes
336 # custom fields changes
336 custom_values.each {|c|
337 custom_values.each {|c|
337 next if (@custom_values_before_change[c.custom_field_id]==c.value ||
338 next if (@custom_values_before_change[c.custom_field_id]==c.value ||
338 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
339 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
339 @current_journal.details << JournalDetail.new(:property => 'cf',
340 @current_journal.details << JournalDetail.new(:property => 'cf',
340 :prop_key => c.custom_field_id,
341 :prop_key => c.custom_field_id,
341 :old_value => @custom_values_before_change[c.custom_field_id],
342 :old_value => @custom_values_before_change[c.custom_field_id],
342 :value => c.value)
343 :value => c.value)
343 }
344 }
344 @current_journal.save
345 @current_journal.save
345 end
346 end
346 end
347 end
347 end
348 end
@@ -1,393 +1,403
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 < ActiveSupport::TestCase
20 class IssueTest < ActiveSupport::TestCase
21 fixtures :projects, :users, :members, :member_roles, :roles,
21 fixtures :projects, :users, :members, :member_roles, :roles,
22 :trackers, :projects_trackers,
22 :trackers, :projects_trackers,
23 :versions,
23 :versions,
24 :issue_statuses, :issue_categories, :issue_relations, :workflows,
24 :issue_statuses, :issue_categories, :issue_relations, :workflows,
25 :enumerations,
25 :enumerations,
26 :issues,
26 :issues,
27 :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values,
27 :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values,
28 :time_entries
28 :time_entries
29
29
30 def test_create
30 def test_create
31 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'test_create', :description => 'IssueTest#test_create', :estimated_hours => '1:30')
31 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'test_create', :description => 'IssueTest#test_create', :estimated_hours => '1:30')
32 assert issue.save
32 assert issue.save
33 issue.reload
33 issue.reload
34 assert_equal 1.5, issue.estimated_hours
34 assert_equal 1.5, issue.estimated_hours
35 end
35 end
36
36
37 def test_create_minimal
37 def test_create_minimal
38 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'test_create')
38 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'test_create')
39 assert issue.save
39 assert issue.save
40 assert issue.description.nil?
40 assert issue.description.nil?
41 end
41 end
42
42
43 def test_create_with_required_custom_field
43 def test_create_with_required_custom_field
44 field = IssueCustomField.find_by_name('Database')
44 field = IssueCustomField.find_by_name('Database')
45 field.update_attribute(:is_required, true)
45 field.update_attribute(:is_required, true)
46
46
47 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => 'test_create', :description => 'IssueTest#test_create_with_required_custom_field')
47 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => 'test_create', :description => 'IssueTest#test_create_with_required_custom_field')
48 assert issue.available_custom_fields.include?(field)
48 assert issue.available_custom_fields.include?(field)
49 # No value for the custom field
49 # No value for the custom field
50 assert !issue.save
50 assert !issue.save
51 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
51 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
52 # Blank value
52 # Blank value
53 issue.custom_field_values = { field.id => '' }
53 issue.custom_field_values = { field.id => '' }
54 assert !issue.save
54 assert !issue.save
55 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
55 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
56 # Invalid value
56 # Invalid value
57 issue.custom_field_values = { field.id => 'SQLServer' }
57 issue.custom_field_values = { field.id => 'SQLServer' }
58 assert !issue.save
58 assert !issue.save
59 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
59 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
60 # Valid value
60 # Valid value
61 issue.custom_field_values = { field.id => 'PostgreSQL' }
61 issue.custom_field_values = { field.id => 'PostgreSQL' }
62 assert issue.save
62 assert issue.save
63 issue.reload
63 issue.reload
64 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
64 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
65 end
65 end
66
66
67 def test_visible_scope_for_anonymous
67 def test_visible_scope_for_anonymous
68 # Anonymous user should see issues of public projects only
68 # Anonymous user should see issues of public projects only
69 issues = Issue.visible(User.anonymous).all
69 issues = Issue.visible(User.anonymous).all
70 assert issues.any?
70 assert issues.any?
71 assert_nil issues.detect {|issue| !issue.project.is_public?}
71 assert_nil issues.detect {|issue| !issue.project.is_public?}
72 # Anonymous user should not see issues without permission
72 # Anonymous user should not see issues without permission
73 Role.anonymous.remove_permission!(:view_issues)
73 Role.anonymous.remove_permission!(:view_issues)
74 issues = Issue.visible(User.anonymous).all
74 issues = Issue.visible(User.anonymous).all
75 assert issues.empty?
75 assert issues.empty?
76 end
76 end
77
77
78 def test_visible_scope_for_user
78 def test_visible_scope_for_user
79 user = User.find(9)
79 user = User.find(9)
80 assert user.projects.empty?
80 assert user.projects.empty?
81 # Non member user should see issues of public projects only
81 # Non member user should see issues of public projects only
82 issues = Issue.visible(user).all
82 issues = Issue.visible(user).all
83 assert issues.any?
83 assert issues.any?
84 assert_nil issues.detect {|issue| !issue.project.is_public?}
84 assert_nil issues.detect {|issue| !issue.project.is_public?}
85 # Non member user should not see issues without permission
85 # Non member user should not see issues without permission
86 Role.non_member.remove_permission!(:view_issues)
86 Role.non_member.remove_permission!(:view_issues)
87 user.reload
87 user.reload
88 issues = Issue.visible(user).all
88 issues = Issue.visible(user).all
89 assert issues.empty?
89 assert issues.empty?
90 # User should see issues of projects for which he has view_issues permissions only
90 # User should see issues of projects for which he has view_issues permissions only
91 Member.create!(:principal => user, :project_id => 2, :role_ids => [1])
91 Member.create!(:principal => user, :project_id => 2, :role_ids => [1])
92 user.reload
92 user.reload
93 issues = Issue.visible(user).all
93 issues = Issue.visible(user).all
94 assert issues.any?
94 assert issues.any?
95 assert_nil issues.detect {|issue| issue.project_id != 2}
95 assert_nil issues.detect {|issue| issue.project_id != 2}
96 end
96 end
97
97
98 def test_visible_scope_for_admin
98 def test_visible_scope_for_admin
99 user = User.find(1)
99 user = User.find(1)
100 user.members.each(&:destroy)
100 user.members.each(&:destroy)
101 assert user.projects.empty?
101 assert user.projects.empty?
102 issues = Issue.visible(user).all
102 issues = Issue.visible(user).all
103 assert issues.any?
103 assert issues.any?
104 # Admin should see issues on private projects that he does not belong to
104 # Admin should see issues on private projects that he does not belong to
105 assert issues.detect {|issue| !issue.project.is_public?}
105 assert issues.detect {|issue| !issue.project.is_public?}
106 end
106 end
107
107
108 def test_errors_full_messages_should_include_custom_fields_errors
108 def test_errors_full_messages_should_include_custom_fields_errors
109 field = IssueCustomField.find_by_name('Database')
109 field = IssueCustomField.find_by_name('Database')
110
110
111 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => 'test_create', :description => 'IssueTest#test_create_with_required_custom_field')
111 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => 'test_create', :description => 'IssueTest#test_create_with_required_custom_field')
112 assert issue.available_custom_fields.include?(field)
112 assert issue.available_custom_fields.include?(field)
113 # Invalid value
113 # Invalid value
114 issue.custom_field_values = { field.id => 'SQLServer' }
114 issue.custom_field_values = { field.id => 'SQLServer' }
115
115
116 assert !issue.valid?
116 assert !issue.valid?
117 assert_equal 1, issue.errors.full_messages.size
117 assert_equal 1, issue.errors.full_messages.size
118 assert_equal "Database #{I18n.translate('activerecord.errors.messages.inclusion')}", issue.errors.full_messages.first
118 assert_equal "Database #{I18n.translate('activerecord.errors.messages.inclusion')}", issue.errors.full_messages.first
119 end
119 end
120
120
121 def test_update_issue_with_required_custom_field
121 def test_update_issue_with_required_custom_field
122 field = IssueCustomField.find_by_name('Database')
122 field = IssueCustomField.find_by_name('Database')
123 field.update_attribute(:is_required, true)
123 field.update_attribute(:is_required, true)
124
124
125 issue = Issue.find(1)
125 issue = Issue.find(1)
126 assert_nil issue.custom_value_for(field)
126 assert_nil issue.custom_value_for(field)
127 assert issue.available_custom_fields.include?(field)
127 assert issue.available_custom_fields.include?(field)
128 # No change to custom values, issue can be saved
128 # No change to custom values, issue can be saved
129 assert issue.save
129 assert issue.save
130 # Blank value
130 # Blank value
131 issue.custom_field_values = { field.id => '' }
131 issue.custom_field_values = { field.id => '' }
132 assert !issue.save
132 assert !issue.save
133 # Valid value
133 # Valid value
134 issue.custom_field_values = { field.id => 'PostgreSQL' }
134 issue.custom_field_values = { field.id => 'PostgreSQL' }
135 assert issue.save
135 assert issue.save
136 issue.reload
136 issue.reload
137 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
137 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
138 end
138 end
139
139
140 def test_should_not_update_attributes_if_custom_fields_validation_fails
140 def test_should_not_update_attributes_if_custom_fields_validation_fails
141 issue = Issue.find(1)
141 issue = Issue.find(1)
142 field = IssueCustomField.find_by_name('Database')
142 field = IssueCustomField.find_by_name('Database')
143 assert issue.available_custom_fields.include?(field)
143 assert issue.available_custom_fields.include?(field)
144
144
145 issue.custom_field_values = { field.id => 'Invalid' }
145 issue.custom_field_values = { field.id => 'Invalid' }
146 issue.subject = 'Should be not be saved'
146 issue.subject = 'Should be not be saved'
147 assert !issue.save
147 assert !issue.save
148
148
149 issue.reload
149 issue.reload
150 assert_equal "Can't print recipes", issue.subject
150 assert_equal "Can't print recipes", issue.subject
151 end
151 end
152
152
153 def test_should_not_recreate_custom_values_objects_on_update
153 def test_should_not_recreate_custom_values_objects_on_update
154 field = IssueCustomField.find_by_name('Database')
154 field = IssueCustomField.find_by_name('Database')
155
155
156 issue = Issue.find(1)
156 issue = Issue.find(1)
157 issue.custom_field_values = { field.id => 'PostgreSQL' }
157 issue.custom_field_values = { field.id => 'PostgreSQL' }
158 assert issue.save
158 assert issue.save
159 custom_value = issue.custom_value_for(field)
159 custom_value = issue.custom_value_for(field)
160 issue.reload
160 issue.reload
161 issue.custom_field_values = { field.id => 'MySQL' }
161 issue.custom_field_values = { field.id => 'MySQL' }
162 assert issue.save
162 assert issue.save
163 issue.reload
163 issue.reload
164 assert_equal custom_value.id, issue.custom_value_for(field).id
164 assert_equal custom_value.id, issue.custom_value_for(field).id
165 end
165 end
166
166
167 def test_category_based_assignment
167 def test_category_based_assignment
168 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'Assignment test', :description => 'Assignment test', :category_id => 1)
168 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'Assignment test', :description => 'Assignment test', :category_id => 1)
169 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
169 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
170 end
170 end
171
171
172 def test_copy
172 def test_copy
173 issue = Issue.new.copy_from(1)
173 issue = Issue.new.copy_from(1)
174 assert issue.save
174 assert issue.save
175 issue.reload
175 issue.reload
176 orig = Issue.find(1)
176 orig = Issue.find(1)
177 assert_equal orig.subject, issue.subject
177 assert_equal orig.subject, issue.subject
178 assert_equal orig.tracker, issue.tracker
178 assert_equal orig.tracker, issue.tracker
179 assert_equal orig.custom_values.first.value, issue.custom_values.first.value
179 assert_equal orig.custom_values.first.value, issue.custom_values.first.value
180 end
180 end
181
182 def test_copy_should_copy_status
183 orig = Issue.find(8)
184 assert orig.status != IssueStatus.default
185
186 issue = Issue.new.copy_from(orig)
187 assert issue.save
188 issue.reload
189 assert_equal orig.status, issue.status
190 end
181
191
182 def test_should_close_duplicates
192 def test_should_close_duplicates
183 # Create 3 issues
193 # Create 3 issues
184 issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'Duplicates test', :description => 'Duplicates test')
194 issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'Duplicates test', :description => 'Duplicates test')
185 assert issue1.save
195 assert issue1.save
186 issue2 = issue1.clone
196 issue2 = issue1.clone
187 assert issue2.save
197 assert issue2.save
188 issue3 = issue1.clone
198 issue3 = issue1.clone
189 assert issue3.save
199 assert issue3.save
190
200
191 # 2 is a dupe of 1
201 # 2 is a dupe of 1
192 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
202 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
193 # And 3 is a dupe of 2
203 # And 3 is a dupe of 2
194 IssueRelation.create(:issue_from => issue3, :issue_to => issue2, :relation_type => IssueRelation::TYPE_DUPLICATES)
204 IssueRelation.create(:issue_from => issue3, :issue_to => issue2, :relation_type => IssueRelation::TYPE_DUPLICATES)
195 # And 3 is a dupe of 1 (circular duplicates)
205 # And 3 is a dupe of 1 (circular duplicates)
196 IssueRelation.create(:issue_from => issue3, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
206 IssueRelation.create(:issue_from => issue3, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
197
207
198 assert issue1.reload.duplicates.include?(issue2)
208 assert issue1.reload.duplicates.include?(issue2)
199
209
200 # Closing issue 1
210 # Closing issue 1
201 issue1.init_journal(User.find(:first), "Closing issue1")
211 issue1.init_journal(User.find(:first), "Closing issue1")
202 issue1.status = IssueStatus.find :first, :conditions => {:is_closed => true}
212 issue1.status = IssueStatus.find :first, :conditions => {:is_closed => true}
203 assert issue1.save
213 assert issue1.save
204 # 2 and 3 should be also closed
214 # 2 and 3 should be also closed
205 assert issue2.reload.closed?
215 assert issue2.reload.closed?
206 assert issue3.reload.closed?
216 assert issue3.reload.closed?
207 end
217 end
208
218
209 def test_should_not_close_duplicated_issue
219 def test_should_not_close_duplicated_issue
210 # Create 3 issues
220 # Create 3 issues
211 issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'Duplicates test', :description => 'Duplicates test')
221 issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'Duplicates test', :description => 'Duplicates test')
212 assert issue1.save
222 assert issue1.save
213 issue2 = issue1.clone
223 issue2 = issue1.clone
214 assert issue2.save
224 assert issue2.save
215
225
216 # 2 is a dupe of 1
226 # 2 is a dupe of 1
217 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
227 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
218 # 2 is a dup of 1 but 1 is not a duplicate of 2
228 # 2 is a dup of 1 but 1 is not a duplicate of 2
219 assert !issue2.reload.duplicates.include?(issue1)
229 assert !issue2.reload.duplicates.include?(issue1)
220
230
221 # Closing issue 2
231 # Closing issue 2
222 issue2.init_journal(User.find(:first), "Closing issue2")
232 issue2.init_journal(User.find(:first), "Closing issue2")
223 issue2.status = IssueStatus.find :first, :conditions => {:is_closed => true}
233 issue2.status = IssueStatus.find :first, :conditions => {:is_closed => true}
224 assert issue2.save
234 assert issue2.save
225 # 1 should not be also closed
235 # 1 should not be also closed
226 assert !issue1.reload.closed?
236 assert !issue1.reload.closed?
227 end
237 end
228
238
229 def test_assignable_versions
239 def test_assignable_versions
230 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
240 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
231 assert_equal ['open'], issue.assignable_versions.collect(&:status).uniq
241 assert_equal ['open'], issue.assignable_versions.collect(&:status).uniq
232 end
242 end
233
243
234 def test_should_not_be_able_to_assign_a_new_issue_to_a_closed_version
244 def test_should_not_be_able_to_assign_a_new_issue_to_a_closed_version
235 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
245 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
236 assert !issue.save
246 assert !issue.save
237 assert_not_nil issue.errors.on(:fixed_version_id)
247 assert_not_nil issue.errors.on(:fixed_version_id)
238 end
248 end
239
249
240 def test_should_not_be_able_to_assign_a_new_issue_to_a_locked_version
250 def test_should_not_be_able_to_assign_a_new_issue_to_a_locked_version
241 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 2, :subject => 'New issue')
251 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 2, :subject => 'New issue')
242 assert !issue.save
252 assert !issue.save
243 assert_not_nil issue.errors.on(:fixed_version_id)
253 assert_not_nil issue.errors.on(:fixed_version_id)
244 end
254 end
245
255
246 def test_should_be_able_to_assign_a_new_issue_to_an_open_version
256 def test_should_be_able_to_assign_a_new_issue_to_an_open_version
247 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 3, :subject => 'New issue')
257 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 3, :subject => 'New issue')
248 assert issue.save
258 assert issue.save
249 end
259 end
250
260
251 def test_should_be_able_to_update_an_issue_assigned_to_a_closed_version
261 def test_should_be_able_to_update_an_issue_assigned_to_a_closed_version
252 issue = Issue.find(11)
262 issue = Issue.find(11)
253 assert_equal 'closed', issue.fixed_version.status
263 assert_equal 'closed', issue.fixed_version.status
254 issue.subject = 'Subject changed'
264 issue.subject = 'Subject changed'
255 assert issue.save
265 assert issue.save
256 end
266 end
257
267
258 def test_should_not_be_able_to_reopen_an_issue_assigned_to_a_closed_version
268 def test_should_not_be_able_to_reopen_an_issue_assigned_to_a_closed_version
259 issue = Issue.find(11)
269 issue = Issue.find(11)
260 issue.status_id = 1
270 issue.status_id = 1
261 assert !issue.save
271 assert !issue.save
262 assert_not_nil issue.errors.on_base
272 assert_not_nil issue.errors.on_base
263 end
273 end
264
274
265 def test_should_be_able_to_reopen_and_reassign_an_issue_assigned_to_a_closed_version
275 def test_should_be_able_to_reopen_and_reassign_an_issue_assigned_to_a_closed_version
266 issue = Issue.find(11)
276 issue = Issue.find(11)
267 issue.status_id = 1
277 issue.status_id = 1
268 issue.fixed_version_id = 3
278 issue.fixed_version_id = 3
269 assert issue.save
279 assert issue.save
270 end
280 end
271
281
272 def test_should_be_able_to_reopen_an_issue_assigned_to_a_locked_version
282 def test_should_be_able_to_reopen_an_issue_assigned_to_a_locked_version
273 issue = Issue.find(12)
283 issue = Issue.find(12)
274 assert_equal 'locked', issue.fixed_version.status
284 assert_equal 'locked', issue.fixed_version.status
275 issue.status_id = 1
285 issue.status_id = 1
276 assert issue.save
286 assert issue.save
277 end
287 end
278
288
279 def test_move_to_another_project_with_same_category
289 def test_move_to_another_project_with_same_category
280 issue = Issue.find(1)
290 issue = Issue.find(1)
281 assert issue.move_to(Project.find(2))
291 assert issue.move_to(Project.find(2))
282 issue.reload
292 issue.reload
283 assert_equal 2, issue.project_id
293 assert_equal 2, issue.project_id
284 # Category changes
294 # Category changes
285 assert_equal 4, issue.category_id
295 assert_equal 4, issue.category_id
286 # Make sure time entries were move to the target project
296 # Make sure time entries were move to the target project
287 assert_equal 2, issue.time_entries.first.project_id
297 assert_equal 2, issue.time_entries.first.project_id
288 end
298 end
289
299
290 def test_move_to_another_project_without_same_category
300 def test_move_to_another_project_without_same_category
291 issue = Issue.find(2)
301 issue = Issue.find(2)
292 assert issue.move_to(Project.find(2))
302 assert issue.move_to(Project.find(2))
293 issue.reload
303 issue.reload
294 assert_equal 2, issue.project_id
304 assert_equal 2, issue.project_id
295 # Category cleared
305 # Category cleared
296 assert_nil issue.category_id
306 assert_nil issue.category_id
297 end
307 end
298
308
299 def test_copy_to_the_same_project
309 def test_copy_to_the_same_project
300 issue = Issue.find(1)
310 issue = Issue.find(1)
301 copy = nil
311 copy = nil
302 assert_difference 'Issue.count' do
312 assert_difference 'Issue.count' do
303 copy = issue.move_to(issue.project, nil, :copy => true)
313 copy = issue.move_to(issue.project, nil, :copy => true)
304 end
314 end
305 assert_kind_of Issue, copy
315 assert_kind_of Issue, copy
306 assert_equal issue.project, copy.project
316 assert_equal issue.project, copy.project
307 assert_equal "125", copy.custom_value_for(2).value
317 assert_equal "125", copy.custom_value_for(2).value
308 end
318 end
309
319
310 def test_copy_to_another_project_and_tracker
320 def test_copy_to_another_project_and_tracker
311 issue = Issue.find(1)
321 issue = Issue.find(1)
312 copy = nil
322 copy = nil
313 assert_difference 'Issue.count' do
323 assert_difference 'Issue.count' do
314 copy = issue.move_to(Project.find(3), Tracker.find(2), :copy => true)
324 copy = issue.move_to(Project.find(3), Tracker.find(2), :copy => true)
315 end
325 end
316 assert_kind_of Issue, copy
326 assert_kind_of Issue, copy
317 assert_equal Project.find(3), copy.project
327 assert_equal Project.find(3), copy.project
318 assert_equal Tracker.find(2), copy.tracker
328 assert_equal Tracker.find(2), copy.tracker
319 # Custom field #2 is not associated with target tracker
329 # Custom field #2 is not associated with target tracker
320 assert_nil copy.custom_value_for(2)
330 assert_nil copy.custom_value_for(2)
321 end
331 end
322
332
323 def test_issue_destroy
333 def test_issue_destroy
324 Issue.find(1).destroy
334 Issue.find(1).destroy
325 assert_nil Issue.find_by_id(1)
335 assert_nil Issue.find_by_id(1)
326 assert_nil TimeEntry.find_by_issue_id(1)
336 assert_nil TimeEntry.find_by_issue_id(1)
327 end
337 end
328
338
329 def test_blocked
339 def test_blocked
330 blocked_issue = Issue.find(9)
340 blocked_issue = Issue.find(9)
331 blocking_issue = Issue.find(10)
341 blocking_issue = Issue.find(10)
332
342
333 assert blocked_issue.blocked?
343 assert blocked_issue.blocked?
334 assert !blocking_issue.blocked?
344 assert !blocking_issue.blocked?
335 end
345 end
336
346
337 def test_blocked_issues_dont_allow_closed_statuses
347 def test_blocked_issues_dont_allow_closed_statuses
338 blocked_issue = Issue.find(9)
348 blocked_issue = Issue.find(9)
339
349
340 allowed_statuses = blocked_issue.new_statuses_allowed_to(users(:users_002))
350 allowed_statuses = blocked_issue.new_statuses_allowed_to(users(:users_002))
341 assert !allowed_statuses.empty?
351 assert !allowed_statuses.empty?
342 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
352 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
343 assert closed_statuses.empty?
353 assert closed_statuses.empty?
344 end
354 end
345
355
346 def test_unblocked_issues_allow_closed_statuses
356 def test_unblocked_issues_allow_closed_statuses
347 blocking_issue = Issue.find(10)
357 blocking_issue = Issue.find(10)
348
358
349 allowed_statuses = blocking_issue.new_statuses_allowed_to(users(:users_002))
359 allowed_statuses = blocking_issue.new_statuses_allowed_to(users(:users_002))
350 assert !allowed_statuses.empty?
360 assert !allowed_statuses.empty?
351 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
361 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
352 assert !closed_statuses.empty?
362 assert !closed_statuses.empty?
353 end
363 end
354
364
355 def test_overdue
365 def test_overdue
356 assert Issue.new(:due_date => 1.day.ago.to_date).overdue?
366 assert Issue.new(:due_date => 1.day.ago.to_date).overdue?
357 assert !Issue.new(:due_date => Date.today).overdue?
367 assert !Issue.new(:due_date => Date.today).overdue?
358 assert !Issue.new(:due_date => 1.day.from_now.to_date).overdue?
368 assert !Issue.new(:due_date => 1.day.from_now.to_date).overdue?
359 assert !Issue.new(:due_date => nil).overdue?
369 assert !Issue.new(:due_date => nil).overdue?
360 assert !Issue.new(:due_date => 1.day.ago.to_date, :status => IssueStatus.find(:first, :conditions => {:is_closed => true})).overdue?
370 assert !Issue.new(:due_date => 1.day.ago.to_date, :status => IssueStatus.find(:first, :conditions => {:is_closed => true})).overdue?
361 end
371 end
362
372
363 def test_assignable_users
373 def test_assignable_users
364 assert_kind_of User, Issue.find(1).assignable_users.first
374 assert_kind_of User, Issue.find(1).assignable_users.first
365 end
375 end
366
376
367 def test_create_should_send_email_notification
377 def test_create_should_send_email_notification
368 ActionMailer::Base.deliveries.clear
378 ActionMailer::Base.deliveries.clear
369 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'test_create', :estimated_hours => '1:30')
379 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'test_create', :estimated_hours => '1:30')
370
380
371 assert issue.save
381 assert issue.save
372 assert_equal 1, ActionMailer::Base.deliveries.size
382 assert_equal 1, ActionMailer::Base.deliveries.size
373 end
383 end
374
384
375 def test_stale_issue_should_not_send_email_notification
385 def test_stale_issue_should_not_send_email_notification
376 ActionMailer::Base.deliveries.clear
386 ActionMailer::Base.deliveries.clear
377 issue = Issue.find(1)
387 issue = Issue.find(1)
378 stale = Issue.find(1)
388 stale = Issue.find(1)
379
389
380 issue.init_journal(User.find(1))
390 issue.init_journal(User.find(1))
381 issue.subject = 'Subjet update'
391 issue.subject = 'Subjet update'
382 assert issue.save
392 assert issue.save
383 assert_equal 1, ActionMailer::Base.deliveries.size
393 assert_equal 1, ActionMailer::Base.deliveries.size
384 ActionMailer::Base.deliveries.clear
394 ActionMailer::Base.deliveries.clear
385
395
386 stale.init_journal(User.find(1))
396 stale.init_journal(User.find(1))
387 stale.subject = 'Another subjet update'
397 stale.subject = 'Another subjet update'
388 assert_raise ActiveRecord::StaleObjectError do
398 assert_raise ActiveRecord::StaleObjectError do
389 stale.save
399 stale.save
390 end
400 end
391 assert ActionMailer::Base.deliveries.empty?
401 assert ActionMailer::Base.deliveries.empty?
392 end
402 end
393 end
403 end
@@ -1,550 +1,560
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 ProjectTest < ActiveSupport::TestCase
20 class ProjectTest < ActiveSupport::TestCase
21 fixtures :projects, :enabled_modules,
21 fixtures :projects, :enabled_modules,
22 :issues, :issue_statuses, :journals, :journal_details,
22 :issues, :issue_statuses, :journals, :journal_details,
23 :users, :members, :member_roles, :roles, :projects_trackers, :trackers, :boards,
23 :users, :members, :member_roles, :roles, :projects_trackers, :trackers, :boards,
24 :queries
24 :queries
25
25
26 def setup
26 def setup
27 @ecookbook = Project.find(1)
27 @ecookbook = Project.find(1)
28 @ecookbook_sub1 = Project.find(3)
28 @ecookbook_sub1 = Project.find(3)
29 User.current = nil
29 User.current = nil
30 end
30 end
31
31
32 should_validate_presence_of :name
32 should_validate_presence_of :name
33 should_validate_presence_of :identifier
33 should_validate_presence_of :identifier
34
34
35 should_validate_uniqueness_of :name
35 should_validate_uniqueness_of :name
36 should_validate_uniqueness_of :identifier
36 should_validate_uniqueness_of :identifier
37
37
38 context "associations" do
38 context "associations" do
39 should_have_many :members
39 should_have_many :members
40 should_have_many :users, :through => :members
40 should_have_many :users, :through => :members
41 should_have_many :member_principals
41 should_have_many :member_principals
42 should_have_many :principals, :through => :member_principals
42 should_have_many :principals, :through => :member_principals
43 should_have_many :enabled_modules
43 should_have_many :enabled_modules
44 should_have_many :issues
44 should_have_many :issues
45 should_have_many :issue_changes, :through => :issues
45 should_have_many :issue_changes, :through => :issues
46 should_have_many :versions
46 should_have_many :versions
47 should_have_many :time_entries
47 should_have_many :time_entries
48 should_have_many :queries
48 should_have_many :queries
49 should_have_many :documents
49 should_have_many :documents
50 should_have_many :news
50 should_have_many :news
51 should_have_many :issue_categories
51 should_have_many :issue_categories
52 should_have_many :boards
52 should_have_many :boards
53 should_have_many :changesets, :through => :repository
53 should_have_many :changesets, :through => :repository
54
54
55 should_have_one :repository
55 should_have_one :repository
56 should_have_one :wiki
56 should_have_one :wiki
57
57
58 should_have_and_belong_to_many :trackers
58 should_have_and_belong_to_many :trackers
59 should_have_and_belong_to_many :issue_custom_fields
59 should_have_and_belong_to_many :issue_custom_fields
60 end
60 end
61
61
62 def test_truth
62 def test_truth
63 assert_kind_of Project, @ecookbook
63 assert_kind_of Project, @ecookbook
64 assert_equal "eCookbook", @ecookbook.name
64 assert_equal "eCookbook", @ecookbook.name
65 end
65 end
66
66
67 def test_update
67 def test_update
68 assert_equal "eCookbook", @ecookbook.name
68 assert_equal "eCookbook", @ecookbook.name
69 @ecookbook.name = "eCook"
69 @ecookbook.name = "eCook"
70 assert @ecookbook.save, @ecookbook.errors.full_messages.join("; ")
70 assert @ecookbook.save, @ecookbook.errors.full_messages.join("; ")
71 @ecookbook.reload
71 @ecookbook.reload
72 assert_equal "eCook", @ecookbook.name
72 assert_equal "eCook", @ecookbook.name
73 end
73 end
74
74
75 def test_validate_identifier
75 def test_validate_identifier
76 to_test = {"abc" => true,
76 to_test = {"abc" => true,
77 "ab12" => true,
77 "ab12" => true,
78 "ab-12" => true,
78 "ab-12" => true,
79 "12" => false,
79 "12" => false,
80 "new" => false}
80 "new" => false}
81
81
82 to_test.each do |identifier, valid|
82 to_test.each do |identifier, valid|
83 p = Project.new
83 p = Project.new
84 p.identifier = identifier
84 p.identifier = identifier
85 p.valid?
85 p.valid?
86 assert_equal valid, p.errors.on('identifier').nil?
86 assert_equal valid, p.errors.on('identifier').nil?
87 end
87 end
88 end
88 end
89
89
90 def test_members_should_be_active_users
90 def test_members_should_be_active_users
91 Project.all.each do |project|
91 Project.all.each do |project|
92 assert_nil project.members.detect {|m| !(m.user.is_a?(User) && m.user.active?) }
92 assert_nil project.members.detect {|m| !(m.user.is_a?(User) && m.user.active?) }
93 end
93 end
94 end
94 end
95
95
96 def test_users_should_be_active_users
96 def test_users_should_be_active_users
97 Project.all.each do |project|
97 Project.all.each do |project|
98 assert_nil project.users.detect {|u| !(u.is_a?(User) && u.active?) }
98 assert_nil project.users.detect {|u| !(u.is_a?(User) && u.active?) }
99 end
99 end
100 end
100 end
101
101
102 def test_archive
102 def test_archive
103 user = @ecookbook.members.first.user
103 user = @ecookbook.members.first.user
104 @ecookbook.archive
104 @ecookbook.archive
105 @ecookbook.reload
105 @ecookbook.reload
106
106
107 assert !@ecookbook.active?
107 assert !@ecookbook.active?
108 assert !user.projects.include?(@ecookbook)
108 assert !user.projects.include?(@ecookbook)
109 # Subproject are also archived
109 # Subproject are also archived
110 assert !@ecookbook.children.empty?
110 assert !@ecookbook.children.empty?
111 assert @ecookbook.descendants.active.empty?
111 assert @ecookbook.descendants.active.empty?
112 end
112 end
113
113
114 def test_unarchive
114 def test_unarchive
115 user = @ecookbook.members.first.user
115 user = @ecookbook.members.first.user
116 @ecookbook.archive
116 @ecookbook.archive
117 # A subproject of an archived project can not be unarchived
117 # A subproject of an archived project can not be unarchived
118 assert !@ecookbook_sub1.unarchive
118 assert !@ecookbook_sub1.unarchive
119
119
120 # Unarchive project
120 # Unarchive project
121 assert @ecookbook.unarchive
121 assert @ecookbook.unarchive
122 @ecookbook.reload
122 @ecookbook.reload
123 assert @ecookbook.active?
123 assert @ecookbook.active?
124 assert user.projects.include?(@ecookbook)
124 assert user.projects.include?(@ecookbook)
125 # Subproject can now be unarchived
125 # Subproject can now be unarchived
126 @ecookbook_sub1.reload
126 @ecookbook_sub1.reload
127 assert @ecookbook_sub1.unarchive
127 assert @ecookbook_sub1.unarchive
128 end
128 end
129
129
130 def test_destroy
130 def test_destroy
131 # 2 active members
131 # 2 active members
132 assert_equal 2, @ecookbook.members.size
132 assert_equal 2, @ecookbook.members.size
133 # and 1 is locked
133 # and 1 is locked
134 assert_equal 3, Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).size
134 assert_equal 3, Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).size
135 # some boards
135 # some boards
136 assert @ecookbook.boards.any?
136 assert @ecookbook.boards.any?
137
137
138 @ecookbook.destroy
138 @ecookbook.destroy
139 # make sure that the project non longer exists
139 # make sure that the project non longer exists
140 assert_raise(ActiveRecord::RecordNotFound) { Project.find(@ecookbook.id) }
140 assert_raise(ActiveRecord::RecordNotFound) { Project.find(@ecookbook.id) }
141 # make sure related data was removed
141 # make sure related data was removed
142 assert Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).empty?
142 assert Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).empty?
143 assert Board.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).empty?
143 assert Board.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).empty?
144 end
144 end
145
145
146 def test_move_an_orphan_project_to_a_root_project
146 def test_move_an_orphan_project_to_a_root_project
147 sub = Project.find(2)
147 sub = Project.find(2)
148 sub.set_parent! @ecookbook
148 sub.set_parent! @ecookbook
149 assert_equal @ecookbook.id, sub.parent.id
149 assert_equal @ecookbook.id, sub.parent.id
150 @ecookbook.reload
150 @ecookbook.reload
151 assert_equal 4, @ecookbook.children.size
151 assert_equal 4, @ecookbook.children.size
152 end
152 end
153
153
154 def test_move_an_orphan_project_to_a_subproject
154 def test_move_an_orphan_project_to_a_subproject
155 sub = Project.find(2)
155 sub = Project.find(2)
156 assert sub.set_parent!(@ecookbook_sub1)
156 assert sub.set_parent!(@ecookbook_sub1)
157 end
157 end
158
158
159 def test_move_a_root_project_to_a_project
159 def test_move_a_root_project_to_a_project
160 sub = @ecookbook
160 sub = @ecookbook
161 assert sub.set_parent!(Project.find(2))
161 assert sub.set_parent!(Project.find(2))
162 end
162 end
163
163
164 def test_should_not_move_a_project_to_its_children
164 def test_should_not_move_a_project_to_its_children
165 sub = @ecookbook
165 sub = @ecookbook
166 assert !(sub.set_parent!(Project.find(3)))
166 assert !(sub.set_parent!(Project.find(3)))
167 end
167 end
168
168
169 def test_set_parent_should_add_roots_in_alphabetical_order
169 def test_set_parent_should_add_roots_in_alphabetical_order
170 ProjectCustomField.delete_all
170 ProjectCustomField.delete_all
171 Project.delete_all
171 Project.delete_all
172 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(nil)
172 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(nil)
173 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(nil)
173 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(nil)
174 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(nil)
174 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(nil)
175 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(nil)
175 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(nil)
176
176
177 assert_equal 4, Project.count
177 assert_equal 4, Project.count
178 assert_equal Project.all.sort_by(&:name), Project.all.sort_by(&:lft)
178 assert_equal Project.all.sort_by(&:name), Project.all.sort_by(&:lft)
179 end
179 end
180
180
181 def test_set_parent_should_add_children_in_alphabetical_order
181 def test_set_parent_should_add_children_in_alphabetical_order
182 ProjectCustomField.delete_all
182 ProjectCustomField.delete_all
183 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
183 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
184 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(parent)
184 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(parent)
185 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(parent)
185 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(parent)
186 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(parent)
186 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(parent)
187 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(parent)
187 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(parent)
188
188
189 parent.reload
189 parent.reload
190 assert_equal 4, parent.children.size
190 assert_equal 4, parent.children.size
191 assert_equal parent.children.sort_by(&:name), parent.children
191 assert_equal parent.children.sort_by(&:name), parent.children
192 end
192 end
193
193
194 def test_rebuild_should_sort_children_alphabetically
194 def test_rebuild_should_sort_children_alphabetically
195 ProjectCustomField.delete_all
195 ProjectCustomField.delete_all
196 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
196 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
197 Project.create!(:name => 'Project C', :identifier => 'project-c').move_to_child_of(parent)
197 Project.create!(:name => 'Project C', :identifier => 'project-c').move_to_child_of(parent)
198 Project.create!(:name => 'Project B', :identifier => 'project-b').move_to_child_of(parent)
198 Project.create!(:name => 'Project B', :identifier => 'project-b').move_to_child_of(parent)
199 Project.create!(:name => 'Project D', :identifier => 'project-d').move_to_child_of(parent)
199 Project.create!(:name => 'Project D', :identifier => 'project-d').move_to_child_of(parent)
200 Project.create!(:name => 'Project A', :identifier => 'project-a').move_to_child_of(parent)
200 Project.create!(:name => 'Project A', :identifier => 'project-a').move_to_child_of(parent)
201
201
202 Project.update_all("lft = NULL, rgt = NULL")
202 Project.update_all("lft = NULL, rgt = NULL")
203 Project.rebuild!
203 Project.rebuild!
204
204
205 parent.reload
205 parent.reload
206 assert_equal 4, parent.children.size
206 assert_equal 4, parent.children.size
207 assert_equal parent.children.sort_by(&:name), parent.children
207 assert_equal parent.children.sort_by(&:name), parent.children
208 end
208 end
209
209
210 def test_parent
210 def test_parent
211 p = Project.find(6).parent
211 p = Project.find(6).parent
212 assert p.is_a?(Project)
212 assert p.is_a?(Project)
213 assert_equal 5, p.id
213 assert_equal 5, p.id
214 end
214 end
215
215
216 def test_ancestors
216 def test_ancestors
217 a = Project.find(6).ancestors
217 a = Project.find(6).ancestors
218 assert a.first.is_a?(Project)
218 assert a.first.is_a?(Project)
219 assert_equal [1, 5], a.collect(&:id)
219 assert_equal [1, 5], a.collect(&:id)
220 end
220 end
221
221
222 def test_root
222 def test_root
223 r = Project.find(6).root
223 r = Project.find(6).root
224 assert r.is_a?(Project)
224 assert r.is_a?(Project)
225 assert_equal 1, r.id
225 assert_equal 1, r.id
226 end
226 end
227
227
228 def test_children
228 def test_children
229 c = Project.find(1).children
229 c = Project.find(1).children
230 assert c.first.is_a?(Project)
230 assert c.first.is_a?(Project)
231 assert_equal [5, 3, 4], c.collect(&:id)
231 assert_equal [5, 3, 4], c.collect(&:id)
232 end
232 end
233
233
234 def test_descendants
234 def test_descendants
235 d = Project.find(1).descendants
235 d = Project.find(1).descendants
236 assert d.first.is_a?(Project)
236 assert d.first.is_a?(Project)
237 assert_equal [5, 6, 3, 4], d.collect(&:id)
237 assert_equal [5, 6, 3, 4], d.collect(&:id)
238 end
238 end
239
239
240 def test_allowed_parents_should_be_empty_for_non_member_user
240 def test_allowed_parents_should_be_empty_for_non_member_user
241 Role.non_member.add_permission!(:add_project)
241 Role.non_member.add_permission!(:add_project)
242 user = User.find(9)
242 user = User.find(9)
243 assert user.memberships.empty?
243 assert user.memberships.empty?
244 User.current = user
244 User.current = user
245 assert Project.new.allowed_parents.empty?
245 assert Project.new.allowed_parents.empty?
246 end
246 end
247
247
248 def test_users_by_role
248 def test_users_by_role
249 users_by_role = Project.find(1).users_by_role
249 users_by_role = Project.find(1).users_by_role
250 assert_kind_of Hash, users_by_role
250 assert_kind_of Hash, users_by_role
251 role = Role.find(1)
251 role = Role.find(1)
252 assert_kind_of Array, users_by_role[role]
252 assert_kind_of Array, users_by_role[role]
253 assert users_by_role[role].include?(User.find(2))
253 assert users_by_role[role].include?(User.find(2))
254 end
254 end
255
255
256 def test_rolled_up_trackers
256 def test_rolled_up_trackers
257 parent = Project.find(1)
257 parent = Project.find(1)
258 parent.trackers = Tracker.find([1,2])
258 parent.trackers = Tracker.find([1,2])
259 child = parent.children.find(3)
259 child = parent.children.find(3)
260
260
261 assert_equal [1, 2], parent.tracker_ids
261 assert_equal [1, 2], parent.tracker_ids
262 assert_equal [2, 3], child.trackers.collect(&:id)
262 assert_equal [2, 3], child.trackers.collect(&:id)
263
263
264 assert_kind_of Tracker, parent.rolled_up_trackers.first
264 assert_kind_of Tracker, parent.rolled_up_trackers.first
265 assert_equal Tracker.find(1), parent.rolled_up_trackers.first
265 assert_equal Tracker.find(1), parent.rolled_up_trackers.first
266
266
267 assert_equal [1, 2, 3], parent.rolled_up_trackers.collect(&:id)
267 assert_equal [1, 2, 3], parent.rolled_up_trackers.collect(&:id)
268 assert_equal [2, 3], child.rolled_up_trackers.collect(&:id)
268 assert_equal [2, 3], child.rolled_up_trackers.collect(&:id)
269 end
269 end
270
270
271 def test_rolled_up_trackers_should_ignore_archived_subprojects
271 def test_rolled_up_trackers_should_ignore_archived_subprojects
272 parent = Project.find(1)
272 parent = Project.find(1)
273 parent.trackers = Tracker.find([1,2])
273 parent.trackers = Tracker.find([1,2])
274 child = parent.children.find(3)
274 child = parent.children.find(3)
275 child.trackers = Tracker.find([1,3])
275 child.trackers = Tracker.find([1,3])
276 parent.children.each(&:archive)
276 parent.children.each(&:archive)
277
277
278 assert_equal [1,2], parent.rolled_up_trackers.collect(&:id)
278 assert_equal [1,2], parent.rolled_up_trackers.collect(&:id)
279 end
279 end
280
280
281 def test_next_identifier
281 def test_next_identifier
282 ProjectCustomField.delete_all
282 ProjectCustomField.delete_all
283 Project.create!(:name => 'last', :identifier => 'p2008040')
283 Project.create!(:name => 'last', :identifier => 'p2008040')
284 assert_equal 'p2008041', Project.next_identifier
284 assert_equal 'p2008041', Project.next_identifier
285 end
285 end
286
286
287 def test_next_identifier_first_project
287 def test_next_identifier_first_project
288 Project.delete_all
288 Project.delete_all
289 assert_nil Project.next_identifier
289 assert_nil Project.next_identifier
290 end
290 end
291
291
292
292
293 def test_enabled_module_names_should_not_recreate_enabled_modules
293 def test_enabled_module_names_should_not_recreate_enabled_modules
294 project = Project.find(1)
294 project = Project.find(1)
295 # Remove one module
295 # Remove one module
296 modules = project.enabled_modules.slice(0..-2)
296 modules = project.enabled_modules.slice(0..-2)
297 assert modules.any?
297 assert modules.any?
298 assert_difference 'EnabledModule.count', -1 do
298 assert_difference 'EnabledModule.count', -1 do
299 project.enabled_module_names = modules.collect(&:name)
299 project.enabled_module_names = modules.collect(&:name)
300 end
300 end
301 project.reload
301 project.reload
302 # Ids should be preserved
302 # Ids should be preserved
303 assert_equal project.enabled_module_ids.sort, modules.collect(&:id).sort
303 assert_equal project.enabled_module_ids.sort, modules.collect(&:id).sort
304 end
304 end
305
305
306 def test_copy_from_existing_project
306 def test_copy_from_existing_project
307 source_project = Project.find(1)
307 source_project = Project.find(1)
308 copied_project = Project.copy_from(1)
308 copied_project = Project.copy_from(1)
309
309
310 assert copied_project
310 assert copied_project
311 # Cleared attributes
311 # Cleared attributes
312 assert copied_project.id.blank?
312 assert copied_project.id.blank?
313 assert copied_project.name.blank?
313 assert copied_project.name.blank?
314 assert copied_project.identifier.blank?
314 assert copied_project.identifier.blank?
315
315
316 # Duplicated attributes
316 # Duplicated attributes
317 assert_equal source_project.description, copied_project.description
317 assert_equal source_project.description, copied_project.description
318 assert_equal source_project.enabled_modules, copied_project.enabled_modules
318 assert_equal source_project.enabled_modules, copied_project.enabled_modules
319 assert_equal source_project.trackers, copied_project.trackers
319 assert_equal source_project.trackers, copied_project.trackers
320
320
321 # Default attributes
321 # Default attributes
322 assert_equal 1, copied_project.status
322 assert_equal 1, copied_project.status
323 end
323 end
324
324
325 def test_activities_should_use_the_system_activities
325 def test_activities_should_use_the_system_activities
326 project = Project.find(1)
326 project = Project.find(1)
327 assert_equal project.activities, TimeEntryActivity.find(:all, :conditions => {:active => true} )
327 assert_equal project.activities, TimeEntryActivity.find(:all, :conditions => {:active => true} )
328 end
328 end
329
329
330
330
331 def test_activities_should_use_the_project_specific_activities
331 def test_activities_should_use_the_project_specific_activities
332 project = Project.find(1)
332 project = Project.find(1)
333 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project})
333 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project})
334 assert overridden_activity.save!
334 assert overridden_activity.save!
335
335
336 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
336 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
337 end
337 end
338
338
339 def test_activities_should_not_include_the_inactive_project_specific_activities
339 def test_activities_should_not_include_the_inactive_project_specific_activities
340 project = Project.find(1)
340 project = Project.find(1)
341 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false})
341 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false})
342 assert overridden_activity.save!
342 assert overridden_activity.save!
343
343
344 assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity found"
344 assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity found"
345 end
345 end
346
346
347 def test_activities_should_not_include_project_specific_activities_from_other_projects
347 def test_activities_should_not_include_project_specific_activities_from_other_projects
348 project = Project.find(1)
348 project = Project.find(1)
349 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(2)})
349 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(2)})
350 assert overridden_activity.save!
350 assert overridden_activity.save!
351
351
352 assert !project.activities.include?(overridden_activity), "Project specific Activity found on a different project"
352 assert !project.activities.include?(overridden_activity), "Project specific Activity found on a different project"
353 end
353 end
354
354
355 def test_activities_should_handle_nils
355 def test_activities_should_handle_nils
356 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(1), :parent => TimeEntryActivity.find(:first)})
356 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(1), :parent => TimeEntryActivity.find(:first)})
357 TimeEntryActivity.delete_all
357 TimeEntryActivity.delete_all
358
358
359 # No activities
359 # No activities
360 project = Project.find(1)
360 project = Project.find(1)
361 assert project.activities.empty?
361 assert project.activities.empty?
362
362
363 # No system, one overridden
363 # No system, one overridden
364 assert overridden_activity.save!
364 assert overridden_activity.save!
365 project.reload
365 project.reload
366 assert_equal [overridden_activity], project.activities
366 assert_equal [overridden_activity], project.activities
367 end
367 end
368
368
369 def test_activities_should_override_system_activities_with_project_activities
369 def test_activities_should_override_system_activities_with_project_activities
370 project = Project.find(1)
370 project = Project.find(1)
371 parent_activity = TimeEntryActivity.find(:first)
371 parent_activity = TimeEntryActivity.find(:first)
372 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => parent_activity})
372 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => parent_activity})
373 assert overridden_activity.save!
373 assert overridden_activity.save!
374
374
375 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
375 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
376 assert !project.activities.include?(parent_activity), "System Activity found when it should have been overridden"
376 assert !project.activities.include?(parent_activity), "System Activity found when it should have been overridden"
377 end
377 end
378
378
379 def test_activities_should_include_inactive_activities_if_specified
379 def test_activities_should_include_inactive_activities_if_specified
380 project = Project.find(1)
380 project = Project.find(1)
381 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false})
381 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false})
382 assert overridden_activity.save!
382 assert overridden_activity.save!
383
383
384 assert project.activities(true).include?(overridden_activity), "Inactive Project specific Activity not found"
384 assert project.activities(true).include?(overridden_activity), "Inactive Project specific Activity not found"
385 end
385 end
386
386
387 def test_close_completed_versions
387 def test_close_completed_versions
388 Version.update_all("status = 'open'")
388 Version.update_all("status = 'open'")
389 project = Project.find(1)
389 project = Project.find(1)
390 assert_not_nil project.versions.detect {|v| v.completed? && v.status == 'open'}
390 assert_not_nil project.versions.detect {|v| v.completed? && v.status == 'open'}
391 assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'}
391 assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'}
392 project.close_completed_versions
392 project.close_completed_versions
393 project.reload
393 project.reload
394 assert_nil project.versions.detect {|v| v.completed? && v.status != 'closed'}
394 assert_nil project.versions.detect {|v| v.completed? && v.status != 'closed'}
395 assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'}
395 assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'}
396 end
396 end
397
397
398 context "Project#copy" do
398 context "Project#copy" do
399 setup do
399 setup do
400 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
400 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
401 Project.destroy_all :identifier => "copy-test"
401 Project.destroy_all :identifier => "copy-test"
402 @source_project = Project.find(2)
402 @source_project = Project.find(2)
403 @project = Project.new(:name => 'Copy Test', :identifier => 'copy-test')
403 @project = Project.new(:name => 'Copy Test', :identifier => 'copy-test')
404 @project.trackers = @source_project.trackers
404 @project.trackers = @source_project.trackers
405 @project.enabled_module_names = @source_project.enabled_modules.collect(&:name)
405 @project.enabled_module_names = @source_project.enabled_modules.collect(&:name)
406 end
406 end
407
407
408 should "copy issues" do
408 should "copy issues" do
409 @source_project.issues << Issue.generate!(:status_id => 5,
410 :subject => "copy issue status",
411 :tracker_id => 1,
412 :assigned_to_id => 2,
413 :project_id => @source_project.id)
409 assert @project.valid?
414 assert @project.valid?
410 assert @project.issues.empty?
415 assert @project.issues.empty?
411 assert @project.copy(@source_project)
416 assert @project.copy(@source_project)
412
417
413 assert_equal @source_project.issues.size, @project.issues.size
418 assert_equal @source_project.issues.size, @project.issues.size
414 @project.issues.each do |issue|
419 @project.issues.each do |issue|
415 assert issue.valid?
420 assert issue.valid?
416 assert ! issue.assigned_to.blank?
421 assert ! issue.assigned_to.blank?
417 assert_equal @project, issue.project
422 assert_equal @project, issue.project
418 end
423 end
424
425 copied_issue = @project.issues.first(:conditions => {:subject => "copy issue status"})
426 assert copied_issue
427 assert copied_issue.status
428 assert_equal "Closed", copied_issue.status.name
419 end
429 end
420
430
421 should "change the new issues to use the copied version" do
431 should "change the new issues to use the copied version" do
422 assigned_version = Version.generate!(:name => "Assigned Issues")
432 assigned_version = Version.generate!(:name => "Assigned Issues")
423 @source_project.versions << assigned_version
433 @source_project.versions << assigned_version
424 assert_equal 1, @source_project.versions.size
434 assert_equal 1, @source_project.versions.size
425 @source_project.issues << Issue.generate!(:fixed_version_id => assigned_version.id,
435 @source_project.issues << Issue.generate!(:fixed_version_id => assigned_version.id,
426 :subject => "change the new issues to use the copied version",
436 :subject => "change the new issues to use the copied version",
427 :tracker_id => 1,
437 :tracker_id => 1,
428 :project_id => @source_project.id)
438 :project_id => @source_project.id)
429
439
430 assert @project.copy(@source_project)
440 assert @project.copy(@source_project)
431 @project.reload
441 @project.reload
432 copied_issue = @project.issues.first(:conditions => {:subject => "change the new issues to use the copied version"})
442 copied_issue = @project.issues.first(:conditions => {:subject => "change the new issues to use the copied version"})
433
443
434 assert copied_issue
444 assert copied_issue
435 assert copied_issue.fixed_version
445 assert copied_issue.fixed_version
436 assert_equal "Assigned Issues", copied_issue.fixed_version.name # Same name
446 assert_equal "Assigned Issues", copied_issue.fixed_version.name # Same name
437 assert_not_equal assigned_version.id, copied_issue.fixed_version.id # Different record
447 assert_not_equal assigned_version.id, copied_issue.fixed_version.id # Different record
438 end
448 end
439
449
440 should "copy members" do
450 should "copy members" do
441 assert @project.valid?
451 assert @project.valid?
442 assert @project.members.empty?
452 assert @project.members.empty?
443 assert @project.copy(@source_project)
453 assert @project.copy(@source_project)
444
454
445 assert_equal @source_project.members.size, @project.members.size
455 assert_equal @source_project.members.size, @project.members.size
446 @project.members.each do |member|
456 @project.members.each do |member|
447 assert member
457 assert member
448 assert_equal @project, member.project
458 assert_equal @project, member.project
449 end
459 end
450 end
460 end
451
461
452 should "copy project specific queries" do
462 should "copy project specific queries" do
453 assert @project.valid?
463 assert @project.valid?
454 assert @project.queries.empty?
464 assert @project.queries.empty?
455 assert @project.copy(@source_project)
465 assert @project.copy(@source_project)
456
466
457 assert_equal @source_project.queries.size, @project.queries.size
467 assert_equal @source_project.queries.size, @project.queries.size
458 @project.queries.each do |query|
468 @project.queries.each do |query|
459 assert query
469 assert query
460 assert_equal @project, query.project
470 assert_equal @project, query.project
461 end
471 end
462 end
472 end
463
473
464 should "copy versions" do
474 should "copy versions" do
465 @source_project.versions << Version.generate!
475 @source_project.versions << Version.generate!
466 @source_project.versions << Version.generate!
476 @source_project.versions << Version.generate!
467
477
468 assert @project.versions.empty?
478 assert @project.versions.empty?
469 assert @project.copy(@source_project)
479 assert @project.copy(@source_project)
470
480
471 assert_equal @source_project.versions.size, @project.versions.size
481 assert_equal @source_project.versions.size, @project.versions.size
472 @project.versions.each do |version|
482 @project.versions.each do |version|
473 assert version
483 assert version
474 assert_equal @project, version.project
484 assert_equal @project, version.project
475 end
485 end
476 end
486 end
477
487
478 should "copy wiki" do
488 should "copy wiki" do
479 assert_difference 'Wiki.count' do
489 assert_difference 'Wiki.count' do
480 assert @project.copy(@source_project)
490 assert @project.copy(@source_project)
481 end
491 end
482
492
483 assert @project.wiki
493 assert @project.wiki
484 assert_not_equal @source_project.wiki, @project.wiki
494 assert_not_equal @source_project.wiki, @project.wiki
485 assert_equal "Start page", @project.wiki.start_page
495 assert_equal "Start page", @project.wiki.start_page
486 end
496 end
487
497
488 should "copy wiki pages and content" do
498 should "copy wiki pages and content" do
489 assert @project.copy(@source_project)
499 assert @project.copy(@source_project)
490
500
491 assert @project.wiki
501 assert @project.wiki
492 assert_equal 1, @project.wiki.pages.length
502 assert_equal 1, @project.wiki.pages.length
493
503
494 @project.wiki.pages.each do |wiki_page|
504 @project.wiki.pages.each do |wiki_page|
495 assert wiki_page.content
505 assert wiki_page.content
496 assert !@source_project.wiki.pages.include?(wiki_page)
506 assert !@source_project.wiki.pages.include?(wiki_page)
497 end
507 end
498 end
508 end
499
509
500 should "copy custom fields"
510 should "copy custom fields"
501
511
502 should "copy issue categories" do
512 should "copy issue categories" do
503 assert @project.copy(@source_project)
513 assert @project.copy(@source_project)
504
514
505 assert_equal 2, @project.issue_categories.size
515 assert_equal 2, @project.issue_categories.size
506 @project.issue_categories.each do |issue_category|
516 @project.issue_categories.each do |issue_category|
507 assert !@source_project.issue_categories.include?(issue_category)
517 assert !@source_project.issue_categories.include?(issue_category)
508 end
518 end
509 end
519 end
510
520
511 should "copy boards" do
521 should "copy boards" do
512 assert @project.copy(@source_project)
522 assert @project.copy(@source_project)
513
523
514 assert_equal 1, @project.boards.size
524 assert_equal 1, @project.boards.size
515 @project.boards.each do |board|
525 @project.boards.each do |board|
516 assert !@source_project.boards.include?(board)
526 assert !@source_project.boards.include?(board)
517 end
527 end
518 end
528 end
519
529
520 should "change the new issues to use the copied issue categories" do
530 should "change the new issues to use the copied issue categories" do
521 issue = Issue.find(4)
531 issue = Issue.find(4)
522 issue.update_attribute(:category_id, 3)
532 issue.update_attribute(:category_id, 3)
523
533
524 assert @project.copy(@source_project)
534 assert @project.copy(@source_project)
525
535
526 @project.issues.each do |issue|
536 @project.issues.each do |issue|
527 assert issue.category
537 assert issue.category
528 assert_equal "Stock management", issue.category.name # Same name
538 assert_equal "Stock management", issue.category.name # Same name
529 assert_not_equal IssueCategory.find(3), issue.category # Different record
539 assert_not_equal IssueCategory.find(3), issue.category # Different record
530 end
540 end
531 end
541 end
532
542
533 should "limit copy with :only option" do
543 should "limit copy with :only option" do
534 assert @project.members.empty?
544 assert @project.members.empty?
535 assert @project.issue_categories.empty?
545 assert @project.issue_categories.empty?
536 assert @source_project.issues.any?
546 assert @source_project.issues.any?
537
547
538 assert @project.copy(@source_project, :only => ['members', 'issue_categories'])
548 assert @project.copy(@source_project, :only => ['members', 'issue_categories'])
539
549
540 assert @project.members.any?
550 assert @project.members.any?
541 assert @project.issue_categories.any?
551 assert @project.issue_categories.any?
542 assert @project.issues.empty?
552 assert @project.issues.empty?
543 end
553 end
544
554
545 should "copy issue relations"
555 should "copy issue relations"
546 should "link issue relations if cross project issue relations are valid"
556 should "link issue relations if cross project issue relations are valid"
547
557
548 end
558 end
549
559
550 end
560 end
General Comments 0
You need to be logged in to leave comments. Login now