##// END OF EJS Templates
Backported r9858 from trunk....
Jean-Philippe Lang -
r9677:1881706df4f1
parent child
Show More
@@ -1,1078 +1,1100
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 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 include Redmine::SafeAttributes
19 include Redmine::SafeAttributes
20
20
21 belongs_to :project
21 belongs_to :project
22 belongs_to :tracker
22 belongs_to :tracker
23 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
23 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
24 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
24 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
25 belongs_to :assigned_to, :class_name => 'Principal', :foreign_key => 'assigned_to_id'
25 belongs_to :assigned_to, :class_name => 'Principal', :foreign_key => 'assigned_to_id'
26 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
26 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
27 belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
27 belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
28 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
28 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
29
29
30 has_many :journals, :as => :journalized, :dependent => :destroy
30 has_many :journals, :as => :journalized, :dependent => :destroy
31 has_many :time_entries, :dependent => :delete_all
31 has_many :time_entries, :dependent => :delete_all
32 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
32 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
33
33
34 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
34 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
35 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
35 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
36
36
37 acts_as_nested_set :scope => 'root_id', :dependent => :destroy
37 acts_as_nested_set :scope => 'root_id', :dependent => :destroy
38 acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed
38 acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed
39 acts_as_customizable
39 acts_as_customizable
40 acts_as_watchable
40 acts_as_watchable
41 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
41 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
42 :include => [:project, :journals],
42 :include => [:project, :journals],
43 # sort by id so that limited eager loading doesn't break with postgresql
43 # sort by id so that limited eager loading doesn't break with postgresql
44 :order_column => "#{table_name}.id"
44 :order_column => "#{table_name}.id"
45 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
45 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
46 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
46 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
47 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
47 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
48
48
49 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
49 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
50 :author_key => :author_id
50 :author_key => :author_id
51
51
52 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
52 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
53
53
54 attr_reader :current_journal
54 attr_reader :current_journal
55
55
56 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
56 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
57
57
58 validates_length_of :subject, :maximum => 255
58 validates_length_of :subject, :maximum => 255
59 validates_inclusion_of :done_ratio, :in => 0..100
59 validates_inclusion_of :done_ratio, :in => 0..100
60 validates_numericality_of :estimated_hours, :allow_nil => true
60 validates_numericality_of :estimated_hours, :allow_nil => true
61 validate :validate_issue
61 validate :validate_issue
62
62
63 named_scope :visible, lambda {|*args| { :include => :project,
63 named_scope :visible, lambda {|*args| { :include => :project,
64 :conditions => Issue.visible_condition(args.shift || User.current, *args) } }
64 :conditions => Issue.visible_condition(args.shift || User.current, *args) } }
65
65
66 named_scope :open, lambda {|*args|
66 named_scope :open, lambda {|*args|
67 is_closed = args.size > 0 ? !args.first : false
67 is_closed = args.size > 0 ? !args.first : false
68 {:conditions => ["#{IssueStatus.table_name}.is_closed = ?", is_closed], :include => :status}
68 {:conditions => ["#{IssueStatus.table_name}.is_closed = ?", is_closed], :include => :status}
69 }
69 }
70
70
71 named_scope :recently_updated, :order => "#{Issue.table_name}.updated_on DESC"
71 named_scope :recently_updated, :order => "#{Issue.table_name}.updated_on DESC"
72 named_scope :with_limit, lambda { |limit| { :limit => limit} }
72 named_scope :with_limit, lambda { |limit| { :limit => limit} }
73 named_scope :on_active_project, :include => [:status, :project, :tracker],
73 named_scope :on_active_project, :include => [:status, :project, :tracker],
74 :conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"]
74 :conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"]
75
75
76 before_create :default_assign
76 before_create :default_assign
77 before_save :close_duplicates, :update_done_ratio_from_issue_status
77 before_save :close_duplicates, :update_done_ratio_from_issue_status
78 after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?}
78 after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?}
79 after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal
79 after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal
80 after_destroy :update_parent_attributes
80 after_destroy :update_parent_attributes
81
81
82 # Returns a SQL conditions string used to find all issues visible by the specified user
82 # Returns a SQL conditions string used to find all issues visible by the specified user
83 def self.visible_condition(user, options={})
83 def self.visible_condition(user, options={})
84 Project.allowed_to_condition(user, :view_issues, options) do |role, user|
84 Project.allowed_to_condition(user, :view_issues, options) do |role, user|
85 case role.issues_visibility
85 case role.issues_visibility
86 when 'all'
86 when 'all'
87 nil
87 nil
88 when 'default'
88 when 'default'
89 user_ids = [user.id] + user.groups.map(&:id)
89 user_ids = [user.id] + user.groups.map(&:id)
90 "(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
90 "(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
91 when 'own'
91 when 'own'
92 user_ids = [user.id] + user.groups.map(&:id)
92 user_ids = [user.id] + user.groups.map(&:id)
93 "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
93 "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
94 else
94 else
95 '1=0'
95 '1=0'
96 end
96 end
97 end
97 end
98 end
98 end
99
99
100 # Returns true if usr or current user is allowed to view the issue
100 # Returns true if usr or current user is allowed to view the issue
101 def visible?(usr=nil)
101 def visible?(usr=nil)
102 (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
102 (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
103 case role.issues_visibility
103 case role.issues_visibility
104 when 'all'
104 when 'all'
105 true
105 true
106 when 'default'
106 when 'default'
107 !self.is_private? || self.author == user || user.is_or_belongs_to?(assigned_to)
107 !self.is_private? || self.author == user || user.is_or_belongs_to?(assigned_to)
108 when 'own'
108 when 'own'
109 self.author == user || user.is_or_belongs_to?(assigned_to)
109 self.author == user || user.is_or_belongs_to?(assigned_to)
110 else
110 else
111 false
111 false
112 end
112 end
113 end
113 end
114 end
114 end
115
115
116 def initialize(attributes=nil, *args)
116 def initialize(attributes=nil, *args)
117 super
117 super
118 if new_record?
118 if new_record?
119 # set default values for new records only
119 # set default values for new records only
120 self.status ||= IssueStatus.default
120 self.status ||= IssueStatus.default
121 self.priority ||= IssuePriority.default
121 self.priority ||= IssuePriority.default
122 self.watcher_user_ids = []
122 self.watcher_user_ids = []
123 end
123 end
124 end
124 end
125
125
126 # AR#Base#destroy would raise and StaleObjectError exception
127 # if the issue was already deleted or updated (non matching lock_version).
128 # This is a problem when bulk deleting issues or deleting a project
129 # (because an issue may already be deleted if its parent was deleted
130 # first).
131 # The issue is reloaded by the nested_set before being deleted so
132 # the lock_version condition should not be an issue but we handle it.
133 def destroy
134 super
135 rescue ActiveRecord::StaleObjectError
136 # Stale or already deleted
137 begin
138 reload
139 rescue ActiveRecord::RecordNotFound
140 # The issue was actually already deleted
141 @destroyed = true
142 return freeze
143 end
144 # The issue was stale, retry to destroy
145 super
146 end
147
126 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
148 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
127 def available_custom_fields
149 def available_custom_fields
128 (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields.all) : []
150 (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields.all) : []
129 end
151 end
130
152
131 # Copies attributes from another issue, arg can be an id or an Issue
153 # Copies attributes from another issue, arg can be an id or an Issue
132 def copy_from(arg, options={})
154 def copy_from(arg, options={})
133 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
155 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
134 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
156 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
135 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
157 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
136 self.status = issue.status
158 self.status = issue.status
137 self.author = User.current
159 self.author = User.current
138 unless options[:attachments] == false
160 unless options[:attachments] == false
139 self.attachments = issue.attachments.map do |attachement|
161 self.attachments = issue.attachments.map do |attachement|
140 attachement.copy(:container => self)
162 attachement.copy(:container => self)
141 end
163 end
142 end
164 end
143 @copied_from = issue
165 @copied_from = issue
144 self
166 self
145 end
167 end
146
168
147 # Returns an unsaved copy of the issue
169 # Returns an unsaved copy of the issue
148 def copy(attributes=nil, copy_options={})
170 def copy(attributes=nil, copy_options={})
149 copy = self.class.new.copy_from(self, copy_options)
171 copy = self.class.new.copy_from(self, copy_options)
150 copy.attributes = attributes if attributes
172 copy.attributes = attributes if attributes
151 copy
173 copy
152 end
174 end
153
175
154 # Returns true if the issue is a copy
176 # Returns true if the issue is a copy
155 def copy?
177 def copy?
156 @copied_from.present?
178 @copied_from.present?
157 end
179 end
158
180
159 # Moves/copies an issue to a new project and tracker
181 # Moves/copies an issue to a new project and tracker
160 # Returns the moved/copied issue on success, false on failure
182 # Returns the moved/copied issue on success, false on failure
161 def move_to_project(new_project, new_tracker=nil, options={})
183 def move_to_project(new_project, new_tracker=nil, options={})
162 ActiveSupport::Deprecation.warn "Issue#move_to_project is deprecated, use #project= instead."
184 ActiveSupport::Deprecation.warn "Issue#move_to_project is deprecated, use #project= instead."
163
185
164 if options[:copy]
186 if options[:copy]
165 issue = self.copy
187 issue = self.copy
166 else
188 else
167 issue = self
189 issue = self
168 end
190 end
169
191
170 issue.init_journal(User.current, options[:notes])
192 issue.init_journal(User.current, options[:notes])
171
193
172 # Preserve previous behaviour
194 # Preserve previous behaviour
173 # #move_to_project doesn't change tracker automatically
195 # #move_to_project doesn't change tracker automatically
174 issue.send :project=, new_project, true
196 issue.send :project=, new_project, true
175 if new_tracker
197 if new_tracker
176 issue.tracker = new_tracker
198 issue.tracker = new_tracker
177 end
199 end
178 # Allow bulk setting of attributes on the issue
200 # Allow bulk setting of attributes on the issue
179 if options[:attributes]
201 if options[:attributes]
180 issue.attributes = options[:attributes]
202 issue.attributes = options[:attributes]
181 end
203 end
182
204
183 issue.save ? issue : false
205 issue.save ? issue : false
184 end
206 end
185
207
186 def status_id=(sid)
208 def status_id=(sid)
187 self.status = nil
209 self.status = nil
188 write_attribute(:status_id, sid)
210 write_attribute(:status_id, sid)
189 end
211 end
190
212
191 def priority_id=(pid)
213 def priority_id=(pid)
192 self.priority = nil
214 self.priority = nil
193 write_attribute(:priority_id, pid)
215 write_attribute(:priority_id, pid)
194 end
216 end
195
217
196 def category_id=(cid)
218 def category_id=(cid)
197 self.category = nil
219 self.category = nil
198 write_attribute(:category_id, cid)
220 write_attribute(:category_id, cid)
199 end
221 end
200
222
201 def fixed_version_id=(vid)
223 def fixed_version_id=(vid)
202 self.fixed_version = nil
224 self.fixed_version = nil
203 write_attribute(:fixed_version_id, vid)
225 write_attribute(:fixed_version_id, vid)
204 end
226 end
205
227
206 def tracker_id=(tid)
228 def tracker_id=(tid)
207 self.tracker = nil
229 self.tracker = nil
208 result = write_attribute(:tracker_id, tid)
230 result = write_attribute(:tracker_id, tid)
209 @custom_field_values = nil
231 @custom_field_values = nil
210 result
232 result
211 end
233 end
212
234
213 def project_id=(project_id)
235 def project_id=(project_id)
214 if project_id.to_s != self.project_id.to_s
236 if project_id.to_s != self.project_id.to_s
215 self.project = (project_id.present? ? Project.find_by_id(project_id) : nil)
237 self.project = (project_id.present? ? Project.find_by_id(project_id) : nil)
216 end
238 end
217 end
239 end
218
240
219 def project=(project, keep_tracker=false)
241 def project=(project, keep_tracker=false)
220 project_was = self.project
242 project_was = self.project
221 write_attribute(:project_id, project ? project.id : nil)
243 write_attribute(:project_id, project ? project.id : nil)
222 association_instance_set('project', project)
244 association_instance_set('project', project)
223 if project_was && project && project_was != project
245 if project_was && project && project_was != project
224 unless keep_tracker || project.trackers.include?(tracker)
246 unless keep_tracker || project.trackers.include?(tracker)
225 self.tracker = project.trackers.first
247 self.tracker = project.trackers.first
226 end
248 end
227 # Reassign to the category with same name if any
249 # Reassign to the category with same name if any
228 if category
250 if category
229 self.category = project.issue_categories.find_by_name(category.name)
251 self.category = project.issue_categories.find_by_name(category.name)
230 end
252 end
231 # Keep the fixed_version if it's still valid in the new_project
253 # Keep the fixed_version if it's still valid in the new_project
232 if fixed_version && fixed_version.project != project && !project.shared_versions.include?(fixed_version)
254 if fixed_version && fixed_version.project != project && !project.shared_versions.include?(fixed_version)
233 self.fixed_version = nil
255 self.fixed_version = nil
234 end
256 end
235 if parent && parent.project_id != project_id
257 if parent && parent.project_id != project_id
236 self.parent_issue_id = nil
258 self.parent_issue_id = nil
237 end
259 end
238 @custom_field_values = nil
260 @custom_field_values = nil
239 end
261 end
240 end
262 end
241
263
242 def description=(arg)
264 def description=(arg)
243 if arg.is_a?(String)
265 if arg.is_a?(String)
244 arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n")
266 arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n")
245 end
267 end
246 write_attribute(:description, arg)
268 write_attribute(:description, arg)
247 end
269 end
248
270
249 # Overrides attributes= so that project and tracker get assigned first
271 # Overrides attributes= so that project and tracker get assigned first
250 def attributes_with_project_and_tracker_first=(new_attributes, *args)
272 def attributes_with_project_and_tracker_first=(new_attributes, *args)
251 return if new_attributes.nil?
273 return if new_attributes.nil?
252 attrs = new_attributes.dup
274 attrs = new_attributes.dup
253 attrs.stringify_keys!
275 attrs.stringify_keys!
254
276
255 %w(project project_id tracker tracker_id).each do |attr|
277 %w(project project_id tracker tracker_id).each do |attr|
256 if attrs.has_key?(attr)
278 if attrs.has_key?(attr)
257 send "#{attr}=", attrs.delete(attr)
279 send "#{attr}=", attrs.delete(attr)
258 end
280 end
259 end
281 end
260 send :attributes_without_project_and_tracker_first=, attrs, *args
282 send :attributes_without_project_and_tracker_first=, attrs, *args
261 end
283 end
262 # Do not redefine alias chain on reload (see #4838)
284 # Do not redefine alias chain on reload (see #4838)
263 alias_method_chain(:attributes=, :project_and_tracker_first) unless method_defined?(:attributes_without_project_and_tracker_first=)
285 alias_method_chain(:attributes=, :project_and_tracker_first) unless method_defined?(:attributes_without_project_and_tracker_first=)
264
286
265 def estimated_hours=(h)
287 def estimated_hours=(h)
266 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
288 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
267 end
289 end
268
290
269 safe_attributes 'project_id',
291 safe_attributes 'project_id',
270 :if => lambda {|issue, user|
292 :if => lambda {|issue, user|
271 if issue.new_record?
293 if issue.new_record?
272 issue.copy?
294 issue.copy?
273 elsif user.allowed_to?(:move_issues, issue.project)
295 elsif user.allowed_to?(:move_issues, issue.project)
274 projects = Issue.allowed_target_projects_on_move(user)
296 projects = Issue.allowed_target_projects_on_move(user)
275 projects.include?(issue.project) && projects.size > 1
297 projects.include?(issue.project) && projects.size > 1
276 end
298 end
277 }
299 }
278
300
279 safe_attributes 'tracker_id',
301 safe_attributes 'tracker_id',
280 'status_id',
302 'status_id',
281 'category_id',
303 'category_id',
282 'assigned_to_id',
304 'assigned_to_id',
283 'priority_id',
305 'priority_id',
284 'fixed_version_id',
306 'fixed_version_id',
285 'subject',
307 'subject',
286 'description',
308 'description',
287 'start_date',
309 'start_date',
288 'due_date',
310 'due_date',
289 'done_ratio',
311 'done_ratio',
290 'estimated_hours',
312 'estimated_hours',
291 'custom_field_values',
313 'custom_field_values',
292 'custom_fields',
314 'custom_fields',
293 'lock_version',
315 'lock_version',
294 :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
316 :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
295
317
296 safe_attributes 'status_id',
318 safe_attributes 'status_id',
297 'assigned_to_id',
319 'assigned_to_id',
298 'fixed_version_id',
320 'fixed_version_id',
299 'done_ratio',
321 'done_ratio',
300 'lock_version',
322 'lock_version',
301 :if => lambda {|issue, user| issue.new_statuses_allowed_to(user).any? }
323 :if => lambda {|issue, user| issue.new_statuses_allowed_to(user).any? }
302
324
303 safe_attributes 'watcher_user_ids',
325 safe_attributes 'watcher_user_ids',
304 :if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)}
326 :if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)}
305
327
306 safe_attributes 'is_private',
328 safe_attributes 'is_private',
307 :if => lambda {|issue, user|
329 :if => lambda {|issue, user|
308 user.allowed_to?(:set_issues_private, issue.project) ||
330 user.allowed_to?(:set_issues_private, issue.project) ||
309 (issue.author == user && user.allowed_to?(:set_own_issues_private, issue.project))
331 (issue.author == user && user.allowed_to?(:set_own_issues_private, issue.project))
310 }
332 }
311
333
312 safe_attributes 'parent_issue_id',
334 safe_attributes 'parent_issue_id',
313 :if => lambda {|issue, user| (issue.new_record? || user.allowed_to?(:edit_issues, issue.project)) &&
335 :if => lambda {|issue, user| (issue.new_record? || user.allowed_to?(:edit_issues, issue.project)) &&
314 user.allowed_to?(:manage_subtasks, issue.project)}
336 user.allowed_to?(:manage_subtasks, issue.project)}
315
337
316 # Safely sets attributes
338 # Safely sets attributes
317 # Should be called from controllers instead of #attributes=
339 # Should be called from controllers instead of #attributes=
318 # attr_accessible is too rough because we still want things like
340 # attr_accessible is too rough because we still want things like
319 # Issue.new(:project => foo) to work
341 # Issue.new(:project => foo) to work
320 def safe_attributes=(attrs, user=User.current)
342 def safe_attributes=(attrs, user=User.current)
321 return unless attrs.is_a?(Hash)
343 return unless attrs.is_a?(Hash)
322
344
323 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
345 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
324 attrs = delete_unsafe_attributes(attrs, user)
346 attrs = delete_unsafe_attributes(attrs, user)
325 return if attrs.empty?
347 return if attrs.empty?
326
348
327 # Project and Tracker must be set before since new_statuses_allowed_to depends on it.
349 # Project and Tracker must be set before since new_statuses_allowed_to depends on it.
328 if p = attrs.delete('project_id')
350 if p = attrs.delete('project_id')
329 if allowed_target_projects(user).collect(&:id).include?(p.to_i)
351 if allowed_target_projects(user).collect(&:id).include?(p.to_i)
330 self.project_id = p
352 self.project_id = p
331 end
353 end
332 end
354 end
333
355
334 if t = attrs.delete('tracker_id')
356 if t = attrs.delete('tracker_id')
335 self.tracker_id = t
357 self.tracker_id = t
336 end
358 end
337
359
338 if attrs['status_id']
360 if attrs['status_id']
339 unless new_statuses_allowed_to(user).collect(&:id).include?(attrs['status_id'].to_i)
361 unless new_statuses_allowed_to(user).collect(&:id).include?(attrs['status_id'].to_i)
340 attrs.delete('status_id')
362 attrs.delete('status_id')
341 end
363 end
342 end
364 end
343
365
344 unless leaf?
366 unless leaf?
345 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
367 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
346 end
368 end
347
369
348 if attrs['parent_issue_id'].present?
370 if attrs['parent_issue_id'].present?
349 attrs.delete('parent_issue_id') unless Issue.visible(user).exists?(attrs['parent_issue_id'].to_i)
371 attrs.delete('parent_issue_id') unless Issue.visible(user).exists?(attrs['parent_issue_id'].to_i)
350 end
372 end
351
373
352 # mass-assignment security bypass
374 # mass-assignment security bypass
353 self.send :attributes=, attrs, false
375 self.send :attributes=, attrs, false
354 end
376 end
355
377
356 def done_ratio
378 def done_ratio
357 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
379 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
358 status.default_done_ratio
380 status.default_done_ratio
359 else
381 else
360 read_attribute(:done_ratio)
382 read_attribute(:done_ratio)
361 end
383 end
362 end
384 end
363
385
364 def self.use_status_for_done_ratio?
386 def self.use_status_for_done_ratio?
365 Setting.issue_done_ratio == 'issue_status'
387 Setting.issue_done_ratio == 'issue_status'
366 end
388 end
367
389
368 def self.use_field_for_done_ratio?
390 def self.use_field_for_done_ratio?
369 Setting.issue_done_ratio == 'issue_field'
391 Setting.issue_done_ratio == 'issue_field'
370 end
392 end
371
393
372 def validate_issue
394 def validate_issue
373 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
395 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
374 errors.add :due_date, :not_a_date
396 errors.add :due_date, :not_a_date
375 end
397 end
376
398
377 if self.due_date and self.start_date and self.due_date < self.start_date
399 if self.due_date and self.start_date and self.due_date < self.start_date
378 errors.add :due_date, :greater_than_start_date
400 errors.add :due_date, :greater_than_start_date
379 end
401 end
380
402
381 if start_date && soonest_start && start_date < soonest_start
403 if start_date && soonest_start && start_date < soonest_start
382 errors.add :start_date, :invalid
404 errors.add :start_date, :invalid
383 end
405 end
384
406
385 if fixed_version
407 if fixed_version
386 if !assignable_versions.include?(fixed_version)
408 if !assignable_versions.include?(fixed_version)
387 errors.add :fixed_version_id, :inclusion
409 errors.add :fixed_version_id, :inclusion
388 elsif reopened? && fixed_version.closed?
410 elsif reopened? && fixed_version.closed?
389 errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version)
411 errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version)
390 end
412 end
391 end
413 end
392
414
393 # Checks that the issue can not be added/moved to a disabled tracker
415 # Checks that the issue can not be added/moved to a disabled tracker
394 if project && (tracker_id_changed? || project_id_changed?)
416 if project && (tracker_id_changed? || project_id_changed?)
395 unless project.trackers.include?(tracker)
417 unless project.trackers.include?(tracker)
396 errors.add :tracker_id, :inclusion
418 errors.add :tracker_id, :inclusion
397 end
419 end
398 end
420 end
399
421
400 # Checks parent issue assignment
422 # Checks parent issue assignment
401 if @parent_issue
423 if @parent_issue
402 if @parent_issue.project_id != project_id
424 if @parent_issue.project_id != project_id
403 errors.add :parent_issue_id, :not_same_project
425 errors.add :parent_issue_id, :not_same_project
404 elsif !new_record?
426 elsif !new_record?
405 # moving an existing issue
427 # moving an existing issue
406 if @parent_issue.root_id != root_id
428 if @parent_issue.root_id != root_id
407 # we can always move to another tree
429 # we can always move to another tree
408 elsif move_possible?(@parent_issue)
430 elsif move_possible?(@parent_issue)
409 # move accepted inside tree
431 # move accepted inside tree
410 else
432 else
411 errors.add :parent_issue_id, :not_a_valid_parent
433 errors.add :parent_issue_id, :not_a_valid_parent
412 end
434 end
413 end
435 end
414 end
436 end
415 end
437 end
416
438
417 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
439 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
418 # even if the user turns off the setting later
440 # even if the user turns off the setting later
419 def update_done_ratio_from_issue_status
441 def update_done_ratio_from_issue_status
420 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
442 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
421 self.done_ratio = status.default_done_ratio
443 self.done_ratio = status.default_done_ratio
422 end
444 end
423 end
445 end
424
446
425 def init_journal(user, notes = "")
447 def init_journal(user, notes = "")
426 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
448 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
427 if new_record?
449 if new_record?
428 @current_journal.notify = false
450 @current_journal.notify = false
429 else
451 else
430 @attributes_before_change = attributes.dup
452 @attributes_before_change = attributes.dup
431 @custom_values_before_change = {}
453 @custom_values_before_change = {}
432 self.custom_field_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
454 self.custom_field_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
433 end
455 end
434 # Make sure updated_on is updated when adding a note.
456 # Make sure updated_on is updated when adding a note.
435 updated_on_will_change!
457 updated_on_will_change!
436 @current_journal
458 @current_journal
437 end
459 end
438
460
439 # Returns the id of the last journal or nil
461 # Returns the id of the last journal or nil
440 def last_journal_id
462 def last_journal_id
441 if new_record?
463 if new_record?
442 nil
464 nil
443 else
465 else
444 journals.first(:order => "#{Journal.table_name}.id DESC").try(:id)
466 journals.first(:order => "#{Journal.table_name}.id DESC").try(:id)
445 end
467 end
446 end
468 end
447
469
448 # Return true if the issue is closed, otherwise false
470 # Return true if the issue is closed, otherwise false
449 def closed?
471 def closed?
450 self.status.is_closed?
472 self.status.is_closed?
451 end
473 end
452
474
453 # Return true if the issue is being reopened
475 # Return true if the issue is being reopened
454 def reopened?
476 def reopened?
455 if !new_record? && status_id_changed?
477 if !new_record? && status_id_changed?
456 status_was = IssueStatus.find_by_id(status_id_was)
478 status_was = IssueStatus.find_by_id(status_id_was)
457 status_new = IssueStatus.find_by_id(status_id)
479 status_new = IssueStatus.find_by_id(status_id)
458 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
480 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
459 return true
481 return true
460 end
482 end
461 end
483 end
462 false
484 false
463 end
485 end
464
486
465 # Return true if the issue is being closed
487 # Return true if the issue is being closed
466 def closing?
488 def closing?
467 if !new_record? && status_id_changed?
489 if !new_record? && status_id_changed?
468 status_was = IssueStatus.find_by_id(status_id_was)
490 status_was = IssueStatus.find_by_id(status_id_was)
469 status_new = IssueStatus.find_by_id(status_id)
491 status_new = IssueStatus.find_by_id(status_id)
470 if status_was && status_new && !status_was.is_closed? && status_new.is_closed?
492 if status_was && status_new && !status_was.is_closed? && status_new.is_closed?
471 return true
493 return true
472 end
494 end
473 end
495 end
474 false
496 false
475 end
497 end
476
498
477 # Returns true if the issue is overdue
499 # Returns true if the issue is overdue
478 def overdue?
500 def overdue?
479 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
501 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
480 end
502 end
481
503
482 # Is the amount of work done less than it should for the due date
504 # Is the amount of work done less than it should for the due date
483 def behind_schedule?
505 def behind_schedule?
484 return false if start_date.nil? || due_date.nil?
506 return false if start_date.nil? || due_date.nil?
485 done_date = start_date + ((due_date - start_date+1)* done_ratio/100).floor
507 done_date = start_date + ((due_date - start_date+1)* done_ratio/100).floor
486 return done_date <= Date.today
508 return done_date <= Date.today
487 end
509 end
488
510
489 # Does this issue have children?
511 # Does this issue have children?
490 def children?
512 def children?
491 !leaf?
513 !leaf?
492 end
514 end
493
515
494 # Users the issue can be assigned to
516 # Users the issue can be assigned to
495 def assignable_users
517 def assignable_users
496 users = project.assignable_users
518 users = project.assignable_users
497 users << author if author
519 users << author if author
498 users << assigned_to if assigned_to
520 users << assigned_to if assigned_to
499 users.uniq.sort
521 users.uniq.sort
500 end
522 end
501
523
502 # Versions that the issue can be assigned to
524 # Versions that the issue can be assigned to
503 def assignable_versions
525 def assignable_versions
504 @assignable_versions ||= (project.shared_versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort
526 @assignable_versions ||= (project.shared_versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort
505 end
527 end
506
528
507 # Returns true if this issue is blocked by another issue that is still open
529 # Returns true if this issue is blocked by another issue that is still open
508 def blocked?
530 def blocked?
509 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
531 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
510 end
532 end
511
533
512 # Returns an array of statuses that user is able to apply
534 # Returns an array of statuses that user is able to apply
513 def new_statuses_allowed_to(user=User.current, include_default=false)
535 def new_statuses_allowed_to(user=User.current, include_default=false)
514 if new_record? && @copied_from
536 if new_record? && @copied_from
515 [IssueStatus.default, @copied_from.status].compact.uniq.sort
537 [IssueStatus.default, @copied_from.status].compact.uniq.sort
516 else
538 else
517 initial_status = nil
539 initial_status = nil
518 if new_record?
540 if new_record?
519 initial_status = IssueStatus.default
541 initial_status = IssueStatus.default
520 elsif status_id_was
542 elsif status_id_was
521 initial_status = IssueStatus.find_by_id(status_id_was)
543 initial_status = IssueStatus.find_by_id(status_id_was)
522 end
544 end
523 initial_status ||= status
545 initial_status ||= status
524
546
525 statuses = initial_status.find_new_statuses_allowed_to(
547 statuses = initial_status.find_new_statuses_allowed_to(
526 user.admin ? Role.all : user.roles_for_project(project),
548 user.admin ? Role.all : user.roles_for_project(project),
527 tracker,
549 tracker,
528 author == user,
550 author == user,
529 assigned_to_id_changed? ? assigned_to_id_was == user.id : assigned_to_id == user.id
551 assigned_to_id_changed? ? assigned_to_id_was == user.id : assigned_to_id == user.id
530 )
552 )
531 statuses << initial_status unless statuses.empty?
553 statuses << initial_status unless statuses.empty?
532 statuses << IssueStatus.default if include_default
554 statuses << IssueStatus.default if include_default
533 statuses = statuses.compact.uniq.sort
555 statuses = statuses.compact.uniq.sort
534 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
556 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
535 end
557 end
536 end
558 end
537
559
538 def assigned_to_was
560 def assigned_to_was
539 if assigned_to_id_changed? && assigned_to_id_was.present?
561 if assigned_to_id_changed? && assigned_to_id_was.present?
540 @assigned_to_was ||= User.find_by_id(assigned_to_id_was)
562 @assigned_to_was ||= User.find_by_id(assigned_to_id_was)
541 end
563 end
542 end
564 end
543
565
544 # Returns the mail adresses of users that should be notified
566 # Returns the mail adresses of users that should be notified
545 def recipients
567 def recipients
546 notified = []
568 notified = []
547 # Author and assignee are always notified unless they have been
569 # Author and assignee are always notified unless they have been
548 # locked or don't want to be notified
570 # locked or don't want to be notified
549 notified << author if author
571 notified << author if author
550 if assigned_to
572 if assigned_to
551 notified += (assigned_to.is_a?(Group) ? assigned_to.users : [assigned_to])
573 notified += (assigned_to.is_a?(Group) ? assigned_to.users : [assigned_to])
552 end
574 end
553 if assigned_to_was
575 if assigned_to_was
554 notified += (assigned_to_was.is_a?(Group) ? assigned_to_was.users : [assigned_to_was])
576 notified += (assigned_to_was.is_a?(Group) ? assigned_to_was.users : [assigned_to_was])
555 end
577 end
556 notified = notified.select {|u| u.active? && u.notify_about?(self)}
578 notified = notified.select {|u| u.active? && u.notify_about?(self)}
557
579
558 notified += project.notified_users
580 notified += project.notified_users
559 notified.uniq!
581 notified.uniq!
560 # Remove users that can not view the issue
582 # Remove users that can not view the issue
561 notified.reject! {|user| !visible?(user)}
583 notified.reject! {|user| !visible?(user)}
562 notified.collect(&:mail)
584 notified.collect(&:mail)
563 end
585 end
564
586
565 # Returns the number of hours spent on this issue
587 # Returns the number of hours spent on this issue
566 def spent_hours
588 def spent_hours
567 @spent_hours ||= time_entries.sum(:hours) || 0
589 @spent_hours ||= time_entries.sum(:hours) || 0
568 end
590 end
569
591
570 # Returns the total number of hours spent on this issue and its descendants
592 # Returns the total number of hours spent on this issue and its descendants
571 #
593 #
572 # Example:
594 # Example:
573 # spent_hours => 0.0
595 # spent_hours => 0.0
574 # spent_hours => 50.2
596 # spent_hours => 50.2
575 def total_spent_hours
597 def total_spent_hours
576 @total_spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours",
598 @total_spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours",
577 :joins => "LEFT JOIN #{TimeEntry.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id").to_f || 0.0
599 :joins => "LEFT JOIN #{TimeEntry.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id").to_f || 0.0
578 end
600 end
579
601
580 def relations
602 def relations
581 @relations ||= (relations_from + relations_to).sort
603 @relations ||= (relations_from + relations_to).sort
582 end
604 end
583
605
584 # Preloads relations for a collection of issues
606 # Preloads relations for a collection of issues
585 def self.load_relations(issues)
607 def self.load_relations(issues)
586 if issues.any?
608 if issues.any?
587 relations = IssueRelation.all(:conditions => ["issue_from_id IN (:ids) OR issue_to_id IN (:ids)", {:ids => issues.map(&:id)}])
609 relations = IssueRelation.all(:conditions => ["issue_from_id IN (:ids) OR issue_to_id IN (:ids)", {:ids => issues.map(&:id)}])
588 issues.each do |issue|
610 issues.each do |issue|
589 issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
611 issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
590 end
612 end
591 end
613 end
592 end
614 end
593
615
594 # Preloads visible spent time for a collection of issues
616 # Preloads visible spent time for a collection of issues
595 def self.load_visible_spent_hours(issues, user=User.current)
617 def self.load_visible_spent_hours(issues, user=User.current)
596 if issues.any?
618 if issues.any?
597 hours_by_issue_id = TimeEntry.visible(user).sum(:hours, :group => :issue_id)
619 hours_by_issue_id = TimeEntry.visible(user).sum(:hours, :group => :issue_id)
598 issues.each do |issue|
620 issues.each do |issue|
599 issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0)
621 issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0)
600 end
622 end
601 end
623 end
602 end
624 end
603
625
604 # Finds an issue relation given its id.
626 # Finds an issue relation given its id.
605 def find_relation(relation_id)
627 def find_relation(relation_id)
606 IssueRelation.find(relation_id, :conditions => ["issue_to_id = ? OR issue_from_id = ?", id, id])
628 IssueRelation.find(relation_id, :conditions => ["issue_to_id = ? OR issue_from_id = ?", id, id])
607 end
629 end
608
630
609 def all_dependent_issues(except=[])
631 def all_dependent_issues(except=[])
610 except << self
632 except << self
611 dependencies = []
633 dependencies = []
612 relations_from.each do |relation|
634 relations_from.each do |relation|
613 if relation.issue_to && !except.include?(relation.issue_to)
635 if relation.issue_to && !except.include?(relation.issue_to)
614 dependencies << relation.issue_to
636 dependencies << relation.issue_to
615 dependencies += relation.issue_to.all_dependent_issues(except)
637 dependencies += relation.issue_to.all_dependent_issues(except)
616 end
638 end
617 end
639 end
618 dependencies
640 dependencies
619 end
641 end
620
642
621 # Returns an array of issues that duplicate this one
643 # Returns an array of issues that duplicate this one
622 def duplicates
644 def duplicates
623 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
645 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
624 end
646 end
625
647
626 # Returns the due date or the target due date if any
648 # Returns the due date or the target due date if any
627 # Used on gantt chart
649 # Used on gantt chart
628 def due_before
650 def due_before
629 due_date || (fixed_version ? fixed_version.effective_date : nil)
651 due_date || (fixed_version ? fixed_version.effective_date : nil)
630 end
652 end
631
653
632 # Returns the time scheduled for this issue.
654 # Returns the time scheduled for this issue.
633 #
655 #
634 # Example:
656 # Example:
635 # Start Date: 2/26/09, End Date: 3/04/09
657 # Start Date: 2/26/09, End Date: 3/04/09
636 # duration => 6
658 # duration => 6
637 def duration
659 def duration
638 (start_date && due_date) ? due_date - start_date : 0
660 (start_date && due_date) ? due_date - start_date : 0
639 end
661 end
640
662
641 def soonest_start
663 def soonest_start
642 @soonest_start ||= (
664 @soonest_start ||= (
643 relations_to.collect{|relation| relation.successor_soonest_start} +
665 relations_to.collect{|relation| relation.successor_soonest_start} +
644 ancestors.collect(&:soonest_start)
666 ancestors.collect(&:soonest_start)
645 ).compact.max
667 ).compact.max
646 end
668 end
647
669
648 def reschedule_after(date)
670 def reschedule_after(date)
649 return if date.nil?
671 return if date.nil?
650 if leaf?
672 if leaf?
651 if start_date.nil? || start_date < date
673 if start_date.nil? || start_date < date
652 self.start_date, self.due_date = date, date + duration
674 self.start_date, self.due_date = date, date + duration
653 begin
675 begin
654 save
676 save
655 rescue ActiveRecord::StaleObjectError
677 rescue ActiveRecord::StaleObjectError
656 reload
678 reload
657 self.start_date, self.due_date = date, date + duration
679 self.start_date, self.due_date = date, date + duration
658 save
680 save
659 end
681 end
660 end
682 end
661 else
683 else
662 leaves.each do |leaf|
684 leaves.each do |leaf|
663 leaf.reschedule_after(date)
685 leaf.reschedule_after(date)
664 end
686 end
665 end
687 end
666 end
688 end
667
689
668 def <=>(issue)
690 def <=>(issue)
669 if issue.nil?
691 if issue.nil?
670 -1
692 -1
671 elsif root_id != issue.root_id
693 elsif root_id != issue.root_id
672 (root_id || 0) <=> (issue.root_id || 0)
694 (root_id || 0) <=> (issue.root_id || 0)
673 else
695 else
674 (lft || 0) <=> (issue.lft || 0)
696 (lft || 0) <=> (issue.lft || 0)
675 end
697 end
676 end
698 end
677
699
678 def to_s
700 def to_s
679 "#{tracker} ##{id}: #{subject}"
701 "#{tracker} ##{id}: #{subject}"
680 end
702 end
681
703
682 # Returns a string of css classes that apply to the issue
704 # Returns a string of css classes that apply to the issue
683 def css_classes
705 def css_classes
684 s = "issue status-#{status.position} priority-#{priority.position}"
706 s = "issue status-#{status.position} priority-#{priority.position}"
685 s << ' closed' if closed?
707 s << ' closed' if closed?
686 s << ' overdue' if overdue?
708 s << ' overdue' if overdue?
687 s << ' child' if child?
709 s << ' child' if child?
688 s << ' parent' unless leaf?
710 s << ' parent' unless leaf?
689 s << ' private' if is_private?
711 s << ' private' if is_private?
690 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
712 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
691 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
713 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
692 s
714 s
693 end
715 end
694
716
695 # Saves an issue and a time_entry from the parameters
717 # Saves an issue and a time_entry from the parameters
696 def save_issue_with_child_records(params, existing_time_entry=nil)
718 def save_issue_with_child_records(params, existing_time_entry=nil)
697 Issue.transaction do
719 Issue.transaction do
698 if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, project)
720 if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, project)
699 @time_entry = existing_time_entry || TimeEntry.new
721 @time_entry = existing_time_entry || TimeEntry.new
700 @time_entry.project = project
722 @time_entry.project = project
701 @time_entry.issue = self
723 @time_entry.issue = self
702 @time_entry.user = User.current
724 @time_entry.user = User.current
703 @time_entry.spent_on = User.current.today
725 @time_entry.spent_on = User.current.today
704 @time_entry.attributes = params[:time_entry]
726 @time_entry.attributes = params[:time_entry]
705 self.time_entries << @time_entry
727 self.time_entries << @time_entry
706 end
728 end
707
729
708 # TODO: Rename hook
730 # TODO: Rename hook
709 Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
731 Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
710 if save
732 if save
711 # TODO: Rename hook
733 # TODO: Rename hook
712 Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
734 Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
713 else
735 else
714 raise ActiveRecord::Rollback
736 raise ActiveRecord::Rollback
715 end
737 end
716 end
738 end
717 end
739 end
718
740
719 # Unassigns issues from +version+ if it's no longer shared with issue's project
741 # Unassigns issues from +version+ if it's no longer shared with issue's project
720 def self.update_versions_from_sharing_change(version)
742 def self.update_versions_from_sharing_change(version)
721 # Update issues assigned to the version
743 # Update issues assigned to the version
722 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
744 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
723 end
745 end
724
746
725 # Unassigns issues from versions that are no longer shared
747 # Unassigns issues from versions that are no longer shared
726 # after +project+ was moved
748 # after +project+ was moved
727 def self.update_versions_from_hierarchy_change(project)
749 def self.update_versions_from_hierarchy_change(project)
728 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
750 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
729 # Update issues of the moved projects and issues assigned to a version of a moved project
751 # Update issues of the moved projects and issues assigned to a version of a moved project
730 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
752 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
731 end
753 end
732
754
733 def parent_issue_id=(arg)
755 def parent_issue_id=(arg)
734 parent_issue_id = arg.blank? ? nil : arg.to_i
756 parent_issue_id = arg.blank? ? nil : arg.to_i
735 if parent_issue_id && @parent_issue = Issue.find_by_id(parent_issue_id)
757 if parent_issue_id && @parent_issue = Issue.find_by_id(parent_issue_id)
736 @parent_issue.id
758 @parent_issue.id
737 else
759 else
738 @parent_issue = nil
760 @parent_issue = nil
739 nil
761 nil
740 end
762 end
741 end
763 end
742
764
743 def parent_issue_id
765 def parent_issue_id
744 if instance_variable_defined? :@parent_issue
766 if instance_variable_defined? :@parent_issue
745 @parent_issue.nil? ? nil : @parent_issue.id
767 @parent_issue.nil? ? nil : @parent_issue.id
746 else
768 else
747 parent_id
769 parent_id
748 end
770 end
749 end
771 end
750
772
751 # Extracted from the ReportsController.
773 # Extracted from the ReportsController.
752 def self.by_tracker(project)
774 def self.by_tracker(project)
753 count_and_group_by(:project => project,
775 count_and_group_by(:project => project,
754 :field => 'tracker_id',
776 :field => 'tracker_id',
755 :joins => Tracker.table_name)
777 :joins => Tracker.table_name)
756 end
778 end
757
779
758 def self.by_version(project)
780 def self.by_version(project)
759 count_and_group_by(:project => project,
781 count_and_group_by(:project => project,
760 :field => 'fixed_version_id',
782 :field => 'fixed_version_id',
761 :joins => Version.table_name)
783 :joins => Version.table_name)
762 end
784 end
763
785
764 def self.by_priority(project)
786 def self.by_priority(project)
765 count_and_group_by(:project => project,
787 count_and_group_by(:project => project,
766 :field => 'priority_id',
788 :field => 'priority_id',
767 :joins => IssuePriority.table_name)
789 :joins => IssuePriority.table_name)
768 end
790 end
769
791
770 def self.by_category(project)
792 def self.by_category(project)
771 count_and_group_by(:project => project,
793 count_and_group_by(:project => project,
772 :field => 'category_id',
794 :field => 'category_id',
773 :joins => IssueCategory.table_name)
795 :joins => IssueCategory.table_name)
774 end
796 end
775
797
776 def self.by_assigned_to(project)
798 def self.by_assigned_to(project)
777 count_and_group_by(:project => project,
799 count_and_group_by(:project => project,
778 :field => 'assigned_to_id',
800 :field => 'assigned_to_id',
779 :joins => User.table_name)
801 :joins => User.table_name)
780 end
802 end
781
803
782 def self.by_author(project)
804 def self.by_author(project)
783 count_and_group_by(:project => project,
805 count_and_group_by(:project => project,
784 :field => 'author_id',
806 :field => 'author_id',
785 :joins => User.table_name)
807 :joins => User.table_name)
786 end
808 end
787
809
788 def self.by_subproject(project)
810 def self.by_subproject(project)
789 ActiveRecord::Base.connection.select_all("select s.id as status_id,
811 ActiveRecord::Base.connection.select_all("select s.id as status_id,
790 s.is_closed as closed,
812 s.is_closed as closed,
791 #{Issue.table_name}.project_id as project_id,
813 #{Issue.table_name}.project_id as project_id,
792 count(#{Issue.table_name}.id) as total
814 count(#{Issue.table_name}.id) as total
793 from
815 from
794 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s
816 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s
795 where
817 where
796 #{Issue.table_name}.status_id=s.id
818 #{Issue.table_name}.status_id=s.id
797 and #{Issue.table_name}.project_id = #{Project.table_name}.id
819 and #{Issue.table_name}.project_id = #{Project.table_name}.id
798 and #{visible_condition(User.current, :project => project, :with_subprojects => true)}
820 and #{visible_condition(User.current, :project => project, :with_subprojects => true)}
799 and #{Issue.table_name}.project_id <> #{project.id}
821 and #{Issue.table_name}.project_id <> #{project.id}
800 group by s.id, s.is_closed, #{Issue.table_name}.project_id") if project.descendants.active.any?
822 group by s.id, s.is_closed, #{Issue.table_name}.project_id") if project.descendants.active.any?
801 end
823 end
802 # End ReportsController extraction
824 # End ReportsController extraction
803
825
804 # Returns an array of projects that user can assign the issue to
826 # Returns an array of projects that user can assign the issue to
805 def allowed_target_projects(user=User.current)
827 def allowed_target_projects(user=User.current)
806 if new_record?
828 if new_record?
807 Project.all(:conditions => Project.allowed_to_condition(user, :add_issues))
829 Project.all(:conditions => Project.allowed_to_condition(user, :add_issues))
808 else
830 else
809 self.class.allowed_target_projects_on_move(user)
831 self.class.allowed_target_projects_on_move(user)
810 end
832 end
811 end
833 end
812
834
813 # Returns an array of projects that user can move issues to
835 # Returns an array of projects that user can move issues to
814 def self.allowed_target_projects_on_move(user=User.current)
836 def self.allowed_target_projects_on_move(user=User.current)
815 Project.all(:conditions => Project.allowed_to_condition(user, :move_issues))
837 Project.all(:conditions => Project.allowed_to_condition(user, :move_issues))
816 end
838 end
817
839
818 private
840 private
819
841
820 def after_project_change
842 def after_project_change
821 # Update project_id on related time entries
843 # Update project_id on related time entries
822 TimeEntry.update_all(["project_id = ?", project_id], {:issue_id => id})
844 TimeEntry.update_all(["project_id = ?", project_id], {:issue_id => id})
823
845
824 # Delete issue relations
846 # Delete issue relations
825 unless Setting.cross_project_issue_relations?
847 unless Setting.cross_project_issue_relations?
826 relations_from.clear
848 relations_from.clear
827 relations_to.clear
849 relations_to.clear
828 end
850 end
829
851
830 # Move subtasks
852 # Move subtasks
831 children.each do |child|
853 children.each do |child|
832 # Change project and keep project
854 # Change project and keep project
833 child.send :project=, project, true
855 child.send :project=, project, true
834 unless child.save
856 unless child.save
835 raise ActiveRecord::Rollback
857 raise ActiveRecord::Rollback
836 end
858 end
837 end
859 end
838 end
860 end
839
861
840 def update_nested_set_attributes
862 def update_nested_set_attributes
841 if root_id.nil?
863 if root_id.nil?
842 # issue was just created
864 # issue was just created
843 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
865 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
844 set_default_left_and_right
866 set_default_left_and_right
845 Issue.update_all("root_id = #{root_id}, lft = #{lft}, rgt = #{rgt}", ["id = ?", id])
867 Issue.update_all("root_id = #{root_id}, lft = #{lft}, rgt = #{rgt}", ["id = ?", id])
846 if @parent_issue
868 if @parent_issue
847 move_to_child_of(@parent_issue)
869 move_to_child_of(@parent_issue)
848 end
870 end
849 reload
871 reload
850 elsif parent_issue_id != parent_id
872 elsif parent_issue_id != parent_id
851 former_parent_id = parent_id
873 former_parent_id = parent_id
852 # moving an existing issue
874 # moving an existing issue
853 if @parent_issue && @parent_issue.root_id == root_id
875 if @parent_issue && @parent_issue.root_id == root_id
854 # inside the same tree
876 # inside the same tree
855 move_to_child_of(@parent_issue)
877 move_to_child_of(@parent_issue)
856 else
878 else
857 # to another tree
879 # to another tree
858 unless root?
880 unless root?
859 move_to_right_of(root)
881 move_to_right_of(root)
860 reload
882 reload
861 end
883 end
862 old_root_id = root_id
884 old_root_id = root_id
863 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
885 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
864 target_maxright = nested_set_scope.maximum(right_column_name) || 0
886 target_maxright = nested_set_scope.maximum(right_column_name) || 0
865 offset = target_maxright + 1 - lft
887 offset = target_maxright + 1 - lft
866 Issue.update_all("root_id = #{root_id}, lft = lft + #{offset}, rgt = rgt + #{offset}",
888 Issue.update_all("root_id = #{root_id}, lft = lft + #{offset}, rgt = rgt + #{offset}",
867 ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
889 ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
868 self[left_column_name] = lft + offset
890 self[left_column_name] = lft + offset
869 self[right_column_name] = rgt + offset
891 self[right_column_name] = rgt + offset
870 if @parent_issue
892 if @parent_issue
871 move_to_child_of(@parent_issue)
893 move_to_child_of(@parent_issue)
872 end
894 end
873 end
895 end
874 reload
896 reload
875 # delete invalid relations of all descendants
897 # delete invalid relations of all descendants
876 self_and_descendants.each do |issue|
898 self_and_descendants.each do |issue|
877 issue.relations.each do |relation|
899 issue.relations.each do |relation|
878 relation.destroy unless relation.valid?
900 relation.destroy unless relation.valid?
879 end
901 end
880 end
902 end
881 # update former parent
903 # update former parent
882 recalculate_attributes_for(former_parent_id) if former_parent_id
904 recalculate_attributes_for(former_parent_id) if former_parent_id
883 end
905 end
884 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
906 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
885 end
907 end
886
908
887 def update_parent_attributes
909 def update_parent_attributes
888 recalculate_attributes_for(parent_id) if parent_id
910 recalculate_attributes_for(parent_id) if parent_id
889 end
911 end
890
912
891 def recalculate_attributes_for(issue_id)
913 def recalculate_attributes_for(issue_id)
892 if issue_id && p = Issue.find_by_id(issue_id)
914 if issue_id && p = Issue.find_by_id(issue_id)
893 # priority = highest priority of children
915 # priority = highest priority of children
894 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :joins => :priority)
916 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :joins => :priority)
895 p.priority = IssuePriority.find_by_position(priority_position)
917 p.priority = IssuePriority.find_by_position(priority_position)
896 end
918 end
897
919
898 # start/due dates = lowest/highest dates of children
920 # start/due dates = lowest/highest dates of children
899 p.start_date = p.children.minimum(:start_date)
921 p.start_date = p.children.minimum(:start_date)
900 p.due_date = p.children.maximum(:due_date)
922 p.due_date = p.children.maximum(:due_date)
901 if p.start_date && p.due_date && p.due_date < p.start_date
923 if p.start_date && p.due_date && p.due_date < p.start_date
902 p.start_date, p.due_date = p.due_date, p.start_date
924 p.start_date, p.due_date = p.due_date, p.start_date
903 end
925 end
904
926
905 # done ratio = weighted average ratio of leaves
927 # done ratio = weighted average ratio of leaves
906 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
928 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
907 leaves_count = p.leaves.count
929 leaves_count = p.leaves.count
908 if leaves_count > 0
930 if leaves_count > 0
909 average = p.leaves.average(:estimated_hours).to_f
931 average = p.leaves.average(:estimated_hours).to_f
910 if average == 0
932 if average == 0
911 average = 1
933 average = 1
912 end
934 end
913 done = p.leaves.sum("COALESCE(estimated_hours, #{average}) * (CASE WHEN is_closed = #{connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)", :joins => :status).to_f
935 done = p.leaves.sum("COALESCE(estimated_hours, #{average}) * (CASE WHEN is_closed = #{connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)", :joins => :status).to_f
914 progress = done / (average * leaves_count)
936 progress = done / (average * leaves_count)
915 p.done_ratio = progress.round
937 p.done_ratio = progress.round
916 end
938 end
917 end
939 end
918
940
919 # estimate = sum of leaves estimates
941 # estimate = sum of leaves estimates
920 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
942 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
921 p.estimated_hours = nil if p.estimated_hours == 0.0
943 p.estimated_hours = nil if p.estimated_hours == 0.0
922
944
923 # ancestors will be recursively updated
945 # ancestors will be recursively updated
924 p.save(false)
946 p.save(false)
925 end
947 end
926 end
948 end
927
949
928 # Update issues so their versions are not pointing to a
950 # Update issues so their versions are not pointing to a
929 # fixed_version that is not shared with the issue's project
951 # fixed_version that is not shared with the issue's project
930 def self.update_versions(conditions=nil)
952 def self.update_versions(conditions=nil)
931 # Only need to update issues with a fixed_version from
953 # Only need to update issues with a fixed_version from
932 # a different project and that is not systemwide shared
954 # a different project and that is not systemwide shared
933 Issue.scoped(:conditions => conditions).all(
955 Issue.scoped(:conditions => conditions).all(
934 :conditions => "#{Issue.table_name}.fixed_version_id IS NOT NULL" +
956 :conditions => "#{Issue.table_name}.fixed_version_id IS NOT NULL" +
935 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
957 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
936 " AND #{Version.table_name}.sharing <> 'system'",
958 " AND #{Version.table_name}.sharing <> 'system'",
937 :include => [:project, :fixed_version]
959 :include => [:project, :fixed_version]
938 ).each do |issue|
960 ).each do |issue|
939 next if issue.project.nil? || issue.fixed_version.nil?
961 next if issue.project.nil? || issue.fixed_version.nil?
940 unless issue.project.shared_versions.include?(issue.fixed_version)
962 unless issue.project.shared_versions.include?(issue.fixed_version)
941 issue.init_journal(User.current)
963 issue.init_journal(User.current)
942 issue.fixed_version = nil
964 issue.fixed_version = nil
943 issue.save
965 issue.save
944 end
966 end
945 end
967 end
946 end
968 end
947
969
948 # Callback on attachment deletion
970 # Callback on attachment deletion
949 def attachment_added(obj)
971 def attachment_added(obj)
950 if @current_journal && !obj.new_record?
972 if @current_journal && !obj.new_record?
951 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :value => obj.filename)
973 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :value => obj.filename)
952 end
974 end
953 end
975 end
954
976
955 # Callback on attachment deletion
977 # Callback on attachment deletion
956 def attachment_removed(obj)
978 def attachment_removed(obj)
957 if @current_journal && !obj.new_record?
979 if @current_journal && !obj.new_record?
958 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :old_value => obj.filename)
980 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :old_value => obj.filename)
959 @current_journal.save
981 @current_journal.save
960 end
982 end
961 end
983 end
962
984
963 # Default assignment based on category
985 # Default assignment based on category
964 def default_assign
986 def default_assign
965 if assigned_to.nil? && category && category.assigned_to
987 if assigned_to.nil? && category && category.assigned_to
966 self.assigned_to = category.assigned_to
988 self.assigned_to = category.assigned_to
967 end
989 end
968 end
990 end
969
991
970 # Updates start/due dates of following issues
992 # Updates start/due dates of following issues
971 def reschedule_following_issues
993 def reschedule_following_issues
972 if start_date_changed? || due_date_changed?
994 if start_date_changed? || due_date_changed?
973 relations_from.each do |relation|
995 relations_from.each do |relation|
974 relation.set_issue_to_dates
996 relation.set_issue_to_dates
975 end
997 end
976 end
998 end
977 end
999 end
978
1000
979 # Closes duplicates if the issue is being closed
1001 # Closes duplicates if the issue is being closed
980 def close_duplicates
1002 def close_duplicates
981 if closing?
1003 if closing?
982 duplicates.each do |duplicate|
1004 duplicates.each do |duplicate|
983 # Reload is need in case the duplicate was updated by a previous duplicate
1005 # Reload is need in case the duplicate was updated by a previous duplicate
984 duplicate.reload
1006 duplicate.reload
985 # Don't re-close it if it's already closed
1007 # Don't re-close it if it's already closed
986 next if duplicate.closed?
1008 next if duplicate.closed?
987 # Same user and notes
1009 # Same user and notes
988 if @current_journal
1010 if @current_journal
989 duplicate.init_journal(@current_journal.user, @current_journal.notes)
1011 duplicate.init_journal(@current_journal.user, @current_journal.notes)
990 end
1012 end
991 duplicate.update_attribute :status, self.status
1013 duplicate.update_attribute :status, self.status
992 end
1014 end
993 end
1015 end
994 end
1016 end
995
1017
996 # Saves the changes in a Journal
1018 # Saves the changes in a Journal
997 # Called after_save
1019 # Called after_save
998 def create_journal
1020 def create_journal
999 if @current_journal
1021 if @current_journal
1000 # attributes changes
1022 # attributes changes
1001 if @attributes_before_change
1023 if @attributes_before_change
1002 (Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on)).each {|c|
1024 (Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on)).each {|c|
1003 before = @attributes_before_change[c]
1025 before = @attributes_before_change[c]
1004 after = send(c)
1026 after = send(c)
1005 next if before == after || (before.blank? && after.blank?)
1027 next if before == after || (before.blank? && after.blank?)
1006 @current_journal.details << JournalDetail.new(:property => 'attr',
1028 @current_journal.details << JournalDetail.new(:property => 'attr',
1007 :prop_key => c,
1029 :prop_key => c,
1008 :old_value => before,
1030 :old_value => before,
1009 :value => after)
1031 :value => after)
1010 }
1032 }
1011 end
1033 end
1012 if @custom_values_before_change
1034 if @custom_values_before_change
1013 # custom fields changes
1035 # custom fields changes
1014 custom_field_values.each {|c|
1036 custom_field_values.each {|c|
1015 before = @custom_values_before_change[c.custom_field_id]
1037 before = @custom_values_before_change[c.custom_field_id]
1016 after = c.value
1038 after = c.value
1017 next if before == after || (before.blank? && after.blank?)
1039 next if before == after || (before.blank? && after.blank?)
1018
1040
1019 if before.is_a?(Array) || after.is_a?(Array)
1041 if before.is_a?(Array) || after.is_a?(Array)
1020 before = [before] unless before.is_a?(Array)
1042 before = [before] unless before.is_a?(Array)
1021 after = [after] unless after.is_a?(Array)
1043 after = [after] unless after.is_a?(Array)
1022
1044
1023 # values removed
1045 # values removed
1024 (before - after).reject(&:blank?).each do |value|
1046 (before - after).reject(&:blank?).each do |value|
1025 @current_journal.details << JournalDetail.new(:property => 'cf',
1047 @current_journal.details << JournalDetail.new(:property => 'cf',
1026 :prop_key => c.custom_field_id,
1048 :prop_key => c.custom_field_id,
1027 :old_value => value,
1049 :old_value => value,
1028 :value => nil)
1050 :value => nil)
1029 end
1051 end
1030 # values added
1052 # values added
1031 (after - before).reject(&:blank?).each do |value|
1053 (after - before).reject(&:blank?).each do |value|
1032 @current_journal.details << JournalDetail.new(:property => 'cf',
1054 @current_journal.details << JournalDetail.new(:property => 'cf',
1033 :prop_key => c.custom_field_id,
1055 :prop_key => c.custom_field_id,
1034 :old_value => nil,
1056 :old_value => nil,
1035 :value => value)
1057 :value => value)
1036 end
1058 end
1037 else
1059 else
1038 @current_journal.details << JournalDetail.new(:property => 'cf',
1060 @current_journal.details << JournalDetail.new(:property => 'cf',
1039 :prop_key => c.custom_field_id,
1061 :prop_key => c.custom_field_id,
1040 :old_value => before,
1062 :old_value => before,
1041 :value => after)
1063 :value => after)
1042 end
1064 end
1043 }
1065 }
1044 end
1066 end
1045 @current_journal.save
1067 @current_journal.save
1046 # reset current journal
1068 # reset current journal
1047 init_journal @current_journal.user, @current_journal.notes
1069 init_journal @current_journal.user, @current_journal.notes
1048 end
1070 end
1049 end
1071 end
1050
1072
1051 # Query generator for selecting groups of issue counts for a project
1073 # Query generator for selecting groups of issue counts for a project
1052 # based on specific criteria
1074 # based on specific criteria
1053 #
1075 #
1054 # Options
1076 # Options
1055 # * project - Project to search in.
1077 # * project - Project to search in.
1056 # * field - String. Issue field to key off of in the grouping.
1078 # * field - String. Issue field to key off of in the grouping.
1057 # * joins - String. The table name to join against.
1079 # * joins - String. The table name to join against.
1058 def self.count_and_group_by(options)
1080 def self.count_and_group_by(options)
1059 project = options.delete(:project)
1081 project = options.delete(:project)
1060 select_field = options.delete(:field)
1082 select_field = options.delete(:field)
1061 joins = options.delete(:joins)
1083 joins = options.delete(:joins)
1062
1084
1063 where = "#{Issue.table_name}.#{select_field}=j.id"
1085 where = "#{Issue.table_name}.#{select_field}=j.id"
1064
1086
1065 ActiveRecord::Base.connection.select_all("select s.id as status_id,
1087 ActiveRecord::Base.connection.select_all("select s.id as status_id,
1066 s.is_closed as closed,
1088 s.is_closed as closed,
1067 j.id as #{select_field},
1089 j.id as #{select_field},
1068 count(#{Issue.table_name}.id) as total
1090 count(#{Issue.table_name}.id) as total
1069 from
1091 from
1070 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s, #{joins} j
1092 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s, #{joins} j
1071 where
1093 where
1072 #{Issue.table_name}.status_id=s.id
1094 #{Issue.table_name}.status_id=s.id
1073 and #{where}
1095 and #{where}
1074 and #{Issue.table_name}.project_id=#{Project.table_name}.id
1096 and #{Issue.table_name}.project_id=#{Project.table_name}.id
1075 and #{visible_condition(User.current, :project => project)}
1097 and #{visible_condition(User.current, :project => project)}
1076 group by s.id, s.is_closed, j.id")
1098 group by s.id, s.is_closed, j.id")
1077 end
1099 end
1078 end
1100 end
@@ -1,1263 +1,1287
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 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.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
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 :groups_users,
22 :groups_users,
23 :trackers, :projects_trackers,
23 :trackers, :projects_trackers,
24 :enabled_modules,
24 :enabled_modules,
25 :versions,
25 :versions,
26 :issue_statuses, :issue_categories, :issue_relations, :workflows,
26 :issue_statuses, :issue_categories, :issue_relations, :workflows,
27 :enumerations,
27 :enumerations,
28 :issues,
28 :issues,
29 :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values,
29 :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values,
30 :time_entries
30 :time_entries
31
31
32 include Redmine::I18n
32 include Redmine::I18n
33
33
34 def test_create
34 def test_create
35 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
35 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
36 :status_id => 1, :priority => IssuePriority.all.first,
36 :status_id => 1, :priority => IssuePriority.all.first,
37 :subject => 'test_create',
37 :subject => 'test_create',
38 :description => 'IssueTest#test_create', :estimated_hours => '1:30')
38 :description => 'IssueTest#test_create', :estimated_hours => '1:30')
39 assert issue.save
39 assert issue.save
40 issue.reload
40 issue.reload
41 assert_equal 1.5, issue.estimated_hours
41 assert_equal 1.5, issue.estimated_hours
42 end
42 end
43
43
44 def test_create_minimal
44 def test_create_minimal
45 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
45 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
46 :status_id => 1, :priority => IssuePriority.all.first,
46 :status_id => 1, :priority => IssuePriority.all.first,
47 :subject => 'test_create')
47 :subject => 'test_create')
48 assert issue.save
48 assert issue.save
49 assert issue.description.nil?
49 assert issue.description.nil?
50 end
50 end
51
51
52 def test_create_with_required_custom_field
52 def test_create_with_required_custom_field
53 set_language_if_valid 'en'
53 set_language_if_valid 'en'
54 field = IssueCustomField.find_by_name('Database')
54 field = IssueCustomField.find_by_name('Database')
55 field.update_attribute(:is_required, true)
55 field.update_attribute(:is_required, true)
56
56
57 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
57 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
58 :status_id => 1, :subject => 'test_create',
58 :status_id => 1, :subject => 'test_create',
59 :description => 'IssueTest#test_create_with_required_custom_field')
59 :description => 'IssueTest#test_create_with_required_custom_field')
60 assert issue.available_custom_fields.include?(field)
60 assert issue.available_custom_fields.include?(field)
61 # No value for the custom field
61 # No value for the custom field
62 assert !issue.save
62 assert !issue.save
63 assert_equal ["Database can't be blank"], issue.errors.full_messages
63 assert_equal ["Database can't be blank"], issue.errors.full_messages
64 # Blank value
64 # Blank value
65 issue.custom_field_values = { field.id => '' }
65 issue.custom_field_values = { field.id => '' }
66 assert !issue.save
66 assert !issue.save
67 assert_equal ["Database can't be blank"], issue.errors.full_messages
67 assert_equal ["Database can't be blank"], issue.errors.full_messages
68 # Invalid value
68 # Invalid value
69 issue.custom_field_values = { field.id => 'SQLServer' }
69 issue.custom_field_values = { field.id => 'SQLServer' }
70 assert !issue.save
70 assert !issue.save
71 assert_equal ["Database is not included in the list"], issue.errors.full_messages
71 assert_equal ["Database is not included in the list"], issue.errors.full_messages
72 # Valid value
72 # Valid value
73 issue.custom_field_values = { field.id => 'PostgreSQL' }
73 issue.custom_field_values = { field.id => 'PostgreSQL' }
74 assert issue.save
74 assert issue.save
75 issue.reload
75 issue.reload
76 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
76 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
77 end
77 end
78
78
79 def test_create_with_group_assignment
79 def test_create_with_group_assignment
80 with_settings :issue_group_assignment => '1' do
80 with_settings :issue_group_assignment => '1' do
81 assert Issue.new(:project_id => 2, :tracker_id => 1, :author_id => 1,
81 assert Issue.new(:project_id => 2, :tracker_id => 1, :author_id => 1,
82 :subject => 'Group assignment',
82 :subject => 'Group assignment',
83 :assigned_to_id => 11).save
83 :assigned_to_id => 11).save
84 issue = Issue.first(:order => 'id DESC')
84 issue = Issue.first(:order => 'id DESC')
85 assert_kind_of Group, issue.assigned_to
85 assert_kind_of Group, issue.assigned_to
86 assert_equal Group.find(11), issue.assigned_to
86 assert_equal Group.find(11), issue.assigned_to
87 end
87 end
88 end
88 end
89
89
90 def assert_visibility_match(user, issues)
90 def assert_visibility_match(user, issues)
91 assert_equal issues.collect(&:id).sort, Issue.all.select {|issue| issue.visible?(user)}.collect(&:id).sort
91 assert_equal issues.collect(&:id).sort, Issue.all.select {|issue| issue.visible?(user)}.collect(&:id).sort
92 end
92 end
93
93
94 def test_visible_scope_for_anonymous
94 def test_visible_scope_for_anonymous
95 # Anonymous user should see issues of public projects only
95 # Anonymous user should see issues of public projects only
96 issues = Issue.visible(User.anonymous).all
96 issues = Issue.visible(User.anonymous).all
97 assert issues.any?
97 assert issues.any?
98 assert_nil issues.detect {|issue| !issue.project.is_public?}
98 assert_nil issues.detect {|issue| !issue.project.is_public?}
99 assert_nil issues.detect {|issue| issue.is_private?}
99 assert_nil issues.detect {|issue| issue.is_private?}
100 assert_visibility_match User.anonymous, issues
100 assert_visibility_match User.anonymous, issues
101 end
101 end
102
102
103 def test_visible_scope_for_anonymous_with_own_issues_visibility
103 def test_visible_scope_for_anonymous_with_own_issues_visibility
104 Role.anonymous.update_attribute :issues_visibility, 'own'
104 Role.anonymous.update_attribute :issues_visibility, 'own'
105 Issue.create!(:project_id => 1, :tracker_id => 1,
105 Issue.create!(:project_id => 1, :tracker_id => 1,
106 :author_id => User.anonymous.id,
106 :author_id => User.anonymous.id,
107 :subject => 'Issue by anonymous')
107 :subject => 'Issue by anonymous')
108
108
109 issues = Issue.visible(User.anonymous).all
109 issues = Issue.visible(User.anonymous).all
110 assert issues.any?
110 assert issues.any?
111 assert_nil issues.detect {|issue| issue.author != User.anonymous}
111 assert_nil issues.detect {|issue| issue.author != User.anonymous}
112 assert_visibility_match User.anonymous, issues
112 assert_visibility_match User.anonymous, issues
113 end
113 end
114
114
115 def test_visible_scope_for_anonymous_without_view_issues_permissions
115 def test_visible_scope_for_anonymous_without_view_issues_permissions
116 # Anonymous user should not see issues without permission
116 # Anonymous user should not see issues without permission
117 Role.anonymous.remove_permission!(:view_issues)
117 Role.anonymous.remove_permission!(:view_issues)
118 issues = Issue.visible(User.anonymous).all
118 issues = Issue.visible(User.anonymous).all
119 assert issues.empty?
119 assert issues.empty?
120 assert_visibility_match User.anonymous, issues
120 assert_visibility_match User.anonymous, issues
121 end
121 end
122
122
123 def test_visible_scope_for_non_member
123 def test_visible_scope_for_non_member
124 user = User.find(9)
124 user = User.find(9)
125 assert user.projects.empty?
125 assert user.projects.empty?
126 # Non member user should see issues of public projects only
126 # Non member user should see issues of public projects only
127 issues = Issue.visible(user).all
127 issues = Issue.visible(user).all
128 assert issues.any?
128 assert issues.any?
129 assert_nil issues.detect {|issue| !issue.project.is_public?}
129 assert_nil issues.detect {|issue| !issue.project.is_public?}
130 assert_nil issues.detect {|issue| issue.is_private?}
130 assert_nil issues.detect {|issue| issue.is_private?}
131 assert_visibility_match user, issues
131 assert_visibility_match user, issues
132 end
132 end
133
133
134 def test_visible_scope_for_non_member_with_own_issues_visibility
134 def test_visible_scope_for_non_member_with_own_issues_visibility
135 Role.non_member.update_attribute :issues_visibility, 'own'
135 Role.non_member.update_attribute :issues_visibility, 'own'
136 Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 9, :subject => 'Issue by non member')
136 Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 9, :subject => 'Issue by non member')
137 user = User.find(9)
137 user = User.find(9)
138
138
139 issues = Issue.visible(user).all
139 issues = Issue.visible(user).all
140 assert issues.any?
140 assert issues.any?
141 assert_nil issues.detect {|issue| issue.author != user}
141 assert_nil issues.detect {|issue| issue.author != user}
142 assert_visibility_match user, issues
142 assert_visibility_match user, issues
143 end
143 end
144
144
145 def test_visible_scope_for_non_member_without_view_issues_permissions
145 def test_visible_scope_for_non_member_without_view_issues_permissions
146 # Non member user should not see issues without permission
146 # Non member user should not see issues without permission
147 Role.non_member.remove_permission!(:view_issues)
147 Role.non_member.remove_permission!(:view_issues)
148 user = User.find(9)
148 user = User.find(9)
149 assert user.projects.empty?
149 assert user.projects.empty?
150 issues = Issue.visible(user).all
150 issues = Issue.visible(user).all
151 assert issues.empty?
151 assert issues.empty?
152 assert_visibility_match user, issues
152 assert_visibility_match user, issues
153 end
153 end
154
154
155 def test_visible_scope_for_member
155 def test_visible_scope_for_member
156 user = User.find(9)
156 user = User.find(9)
157 # User should see issues of projects for which he has view_issues permissions only
157 # User should see issues of projects for which he has view_issues permissions only
158 Role.non_member.remove_permission!(:view_issues)
158 Role.non_member.remove_permission!(:view_issues)
159 Member.create!(:principal => user, :project_id => 3, :role_ids => [2])
159 Member.create!(:principal => user, :project_id => 3, :role_ids => [2])
160 issues = Issue.visible(user).all
160 issues = Issue.visible(user).all
161 assert issues.any?
161 assert issues.any?
162 assert_nil issues.detect {|issue| issue.project_id != 3}
162 assert_nil issues.detect {|issue| issue.project_id != 3}
163 assert_nil issues.detect {|issue| issue.is_private?}
163 assert_nil issues.detect {|issue| issue.is_private?}
164 assert_visibility_match user, issues
164 assert_visibility_match user, issues
165 end
165 end
166
166
167 def test_visible_scope_for_member_with_groups_should_return_assigned_issues
167 def test_visible_scope_for_member_with_groups_should_return_assigned_issues
168 user = User.find(8)
168 user = User.find(8)
169 assert user.groups.any?
169 assert user.groups.any?
170 Member.create!(:principal => user.groups.first, :project_id => 1, :role_ids => [2])
170 Member.create!(:principal => user.groups.first, :project_id => 1, :role_ids => [2])
171 Role.non_member.remove_permission!(:view_issues)
171 Role.non_member.remove_permission!(:view_issues)
172
172
173 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3,
173 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3,
174 :status_id => 1, :priority => IssuePriority.all.first,
174 :status_id => 1, :priority => IssuePriority.all.first,
175 :subject => 'Assignment test',
175 :subject => 'Assignment test',
176 :assigned_to => user.groups.first,
176 :assigned_to => user.groups.first,
177 :is_private => true)
177 :is_private => true)
178
178
179 Role.find(2).update_attribute :issues_visibility, 'default'
179 Role.find(2).update_attribute :issues_visibility, 'default'
180 issues = Issue.visible(User.find(8)).all
180 issues = Issue.visible(User.find(8)).all
181 assert issues.any?
181 assert issues.any?
182 assert issues.include?(issue)
182 assert issues.include?(issue)
183
183
184 Role.find(2).update_attribute :issues_visibility, 'own'
184 Role.find(2).update_attribute :issues_visibility, 'own'
185 issues = Issue.visible(User.find(8)).all
185 issues = Issue.visible(User.find(8)).all
186 assert issues.any?
186 assert issues.any?
187 assert issues.include?(issue)
187 assert issues.include?(issue)
188 end
188 end
189
189
190 def test_visible_scope_for_admin
190 def test_visible_scope_for_admin
191 user = User.find(1)
191 user = User.find(1)
192 user.members.each(&:destroy)
192 user.members.each(&:destroy)
193 assert user.projects.empty?
193 assert user.projects.empty?
194 issues = Issue.visible(user).all
194 issues = Issue.visible(user).all
195 assert issues.any?
195 assert issues.any?
196 # Admin should see issues on private projects that he does not belong to
196 # Admin should see issues on private projects that he does not belong to
197 assert issues.detect {|issue| !issue.project.is_public?}
197 assert issues.detect {|issue| !issue.project.is_public?}
198 # Admin should see private issues of other users
198 # Admin should see private issues of other users
199 assert issues.detect {|issue| issue.is_private? && issue.author != user}
199 assert issues.detect {|issue| issue.is_private? && issue.author != user}
200 assert_visibility_match user, issues
200 assert_visibility_match user, issues
201 end
201 end
202
202
203 def test_visible_scope_with_project
203 def test_visible_scope_with_project
204 project = Project.find(1)
204 project = Project.find(1)
205 issues = Issue.visible(User.find(2), :project => project).all
205 issues = Issue.visible(User.find(2), :project => project).all
206 projects = issues.collect(&:project).uniq
206 projects = issues.collect(&:project).uniq
207 assert_equal 1, projects.size
207 assert_equal 1, projects.size
208 assert_equal project, projects.first
208 assert_equal project, projects.first
209 end
209 end
210
210
211 def test_visible_scope_with_project_and_subprojects
211 def test_visible_scope_with_project_and_subprojects
212 project = Project.find(1)
212 project = Project.find(1)
213 issues = Issue.visible(User.find(2), :project => project, :with_subprojects => true).all
213 issues = Issue.visible(User.find(2), :project => project, :with_subprojects => true).all
214 projects = issues.collect(&:project).uniq
214 projects = issues.collect(&:project).uniq
215 assert projects.size > 1
215 assert projects.size > 1
216 assert_equal [], projects.select {|p| !p.is_or_is_descendant_of?(project)}
216 assert_equal [], projects.select {|p| !p.is_or_is_descendant_of?(project)}
217 end
217 end
218
218
219 def test_visible_and_nested_set_scopes
219 def test_visible_and_nested_set_scopes
220 assert_equal 0, Issue.find(1).descendants.visible.all.size
220 assert_equal 0, Issue.find(1).descendants.visible.all.size
221 end
221 end
222
222
223 def test_open_scope
223 def test_open_scope
224 issues = Issue.open.all
224 issues = Issue.open.all
225 assert_nil issues.detect(&:closed?)
225 assert_nil issues.detect(&:closed?)
226 end
226 end
227
227
228 def test_open_scope_with_arg
228 def test_open_scope_with_arg
229 issues = Issue.open(false).all
229 issues = Issue.open(false).all
230 assert_equal issues, issues.select(&:closed?)
230 assert_equal issues, issues.select(&:closed?)
231 end
231 end
232
232
233 def test_errors_full_messages_should_include_custom_fields_errors
233 def test_errors_full_messages_should_include_custom_fields_errors
234 field = IssueCustomField.find_by_name('Database')
234 field = IssueCustomField.find_by_name('Database')
235
235
236 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
236 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
237 :status_id => 1, :subject => 'test_create',
237 :status_id => 1, :subject => 'test_create',
238 :description => 'IssueTest#test_create_with_required_custom_field')
238 :description => 'IssueTest#test_create_with_required_custom_field')
239 assert issue.available_custom_fields.include?(field)
239 assert issue.available_custom_fields.include?(field)
240 # Invalid value
240 # Invalid value
241 issue.custom_field_values = { field.id => 'SQLServer' }
241 issue.custom_field_values = { field.id => 'SQLServer' }
242
242
243 assert !issue.valid?
243 assert !issue.valid?
244 assert_equal 1, issue.errors.full_messages.size
244 assert_equal 1, issue.errors.full_messages.size
245 assert_equal "Database #{I18n.translate('activerecord.errors.messages.inclusion')}",
245 assert_equal "Database #{I18n.translate('activerecord.errors.messages.inclusion')}",
246 issue.errors.full_messages.first
246 issue.errors.full_messages.first
247 end
247 end
248
248
249 def test_update_issue_with_required_custom_field
249 def test_update_issue_with_required_custom_field
250 field = IssueCustomField.find_by_name('Database')
250 field = IssueCustomField.find_by_name('Database')
251 field.update_attribute(:is_required, true)
251 field.update_attribute(:is_required, true)
252
252
253 issue = Issue.find(1)
253 issue = Issue.find(1)
254 assert_nil issue.custom_value_for(field)
254 assert_nil issue.custom_value_for(field)
255 assert issue.available_custom_fields.include?(field)
255 assert issue.available_custom_fields.include?(field)
256 # No change to custom values, issue can be saved
256 # No change to custom values, issue can be saved
257 assert issue.save
257 assert issue.save
258 # Blank value
258 # Blank value
259 issue.custom_field_values = { field.id => '' }
259 issue.custom_field_values = { field.id => '' }
260 assert !issue.save
260 assert !issue.save
261 # Valid value
261 # Valid value
262 issue.custom_field_values = { field.id => 'PostgreSQL' }
262 issue.custom_field_values = { field.id => 'PostgreSQL' }
263 assert issue.save
263 assert issue.save
264 issue.reload
264 issue.reload
265 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
265 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
266 end
266 end
267
267
268 def test_should_not_update_attributes_if_custom_fields_validation_fails
268 def test_should_not_update_attributes_if_custom_fields_validation_fails
269 issue = Issue.find(1)
269 issue = Issue.find(1)
270 field = IssueCustomField.find_by_name('Database')
270 field = IssueCustomField.find_by_name('Database')
271 assert issue.available_custom_fields.include?(field)
271 assert issue.available_custom_fields.include?(field)
272
272
273 issue.custom_field_values = { field.id => 'Invalid' }
273 issue.custom_field_values = { field.id => 'Invalid' }
274 issue.subject = 'Should be not be saved'
274 issue.subject = 'Should be not be saved'
275 assert !issue.save
275 assert !issue.save
276
276
277 issue.reload
277 issue.reload
278 assert_equal "Can't print recipes", issue.subject
278 assert_equal "Can't print recipes", issue.subject
279 end
279 end
280
280
281 def test_should_not_recreate_custom_values_objects_on_update
281 def test_should_not_recreate_custom_values_objects_on_update
282 field = IssueCustomField.find_by_name('Database')
282 field = IssueCustomField.find_by_name('Database')
283
283
284 issue = Issue.find(1)
284 issue = Issue.find(1)
285 issue.custom_field_values = { field.id => 'PostgreSQL' }
285 issue.custom_field_values = { field.id => 'PostgreSQL' }
286 assert issue.save
286 assert issue.save
287 custom_value = issue.custom_value_for(field)
287 custom_value = issue.custom_value_for(field)
288 issue.reload
288 issue.reload
289 issue.custom_field_values = { field.id => 'MySQL' }
289 issue.custom_field_values = { field.id => 'MySQL' }
290 assert issue.save
290 assert issue.save
291 issue.reload
291 issue.reload
292 assert_equal custom_value.id, issue.custom_value_for(field).id
292 assert_equal custom_value.id, issue.custom_value_for(field).id
293 end
293 end
294
294
295 def test_should_not_update_custom_fields_on_changing_tracker_with_different_custom_fields
295 def test_should_not_update_custom_fields_on_changing_tracker_with_different_custom_fields
296 issue = Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => 'Test', :custom_field_values => {'2' => 'Test'})
296 issue = Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => 'Test', :custom_field_values => {'2' => 'Test'})
297 assert !Tracker.find(2).custom_field_ids.include?(2)
297 assert !Tracker.find(2).custom_field_ids.include?(2)
298
298
299 issue = Issue.find(issue.id)
299 issue = Issue.find(issue.id)
300 issue.attributes = {:tracker_id => 2, :custom_field_values => {'1' => ''}}
300 issue.attributes = {:tracker_id => 2, :custom_field_values => {'1' => ''}}
301
301
302 issue = Issue.find(issue.id)
302 issue = Issue.find(issue.id)
303 custom_value = issue.custom_value_for(2)
303 custom_value = issue.custom_value_for(2)
304 assert_not_nil custom_value
304 assert_not_nil custom_value
305 assert_equal 'Test', custom_value.value
305 assert_equal 'Test', custom_value.value
306 end
306 end
307
307
308 def test_assigning_tracker_id_should_reload_custom_fields_values
308 def test_assigning_tracker_id_should_reload_custom_fields_values
309 issue = Issue.new(:project => Project.find(1))
309 issue = Issue.new(:project => Project.find(1))
310 assert issue.custom_field_values.empty?
310 assert issue.custom_field_values.empty?
311 issue.tracker_id = 1
311 issue.tracker_id = 1
312 assert issue.custom_field_values.any?
312 assert issue.custom_field_values.any?
313 end
313 end
314
314
315 def test_assigning_attributes_should_assign_project_and_tracker_first
315 def test_assigning_attributes_should_assign_project_and_tracker_first
316 seq = sequence('seq')
316 seq = sequence('seq')
317 issue = Issue.new
317 issue = Issue.new
318 issue.expects(:project_id=).in_sequence(seq)
318 issue.expects(:project_id=).in_sequence(seq)
319 issue.expects(:tracker_id=).in_sequence(seq)
319 issue.expects(:tracker_id=).in_sequence(seq)
320 issue.expects(:subject=).in_sequence(seq)
320 issue.expects(:subject=).in_sequence(seq)
321 issue.attributes = {:tracker_id => 2, :project_id => 1, :subject => 'Test'}
321 issue.attributes = {:tracker_id => 2, :project_id => 1, :subject => 'Test'}
322 end
322 end
323
323
324 def test_assigning_tracker_and_custom_fields_should_assign_custom_fields
324 def test_assigning_tracker_and_custom_fields_should_assign_custom_fields
325 attributes = ActiveSupport::OrderedHash.new
325 attributes = ActiveSupport::OrderedHash.new
326 attributes['custom_field_values'] = { '1' => 'MySQL' }
326 attributes['custom_field_values'] = { '1' => 'MySQL' }
327 attributes['tracker_id'] = '1'
327 attributes['tracker_id'] = '1'
328 issue = Issue.new(:project => Project.find(1))
328 issue = Issue.new(:project => Project.find(1))
329 issue.attributes = attributes
329 issue.attributes = attributes
330 assert_equal 'MySQL', issue.custom_field_value(1)
330 assert_equal 'MySQL', issue.custom_field_value(1)
331 end
331 end
332
332
333 def test_should_update_issue_with_disabled_tracker
333 def test_should_update_issue_with_disabled_tracker
334 p = Project.find(1)
334 p = Project.find(1)
335 issue = Issue.find(1)
335 issue = Issue.find(1)
336
336
337 p.trackers.delete(issue.tracker)
337 p.trackers.delete(issue.tracker)
338 assert !p.trackers.include?(issue.tracker)
338 assert !p.trackers.include?(issue.tracker)
339
339
340 issue.reload
340 issue.reload
341 issue.subject = 'New subject'
341 issue.subject = 'New subject'
342 assert issue.save
342 assert issue.save
343 end
343 end
344
344
345 def test_should_not_set_a_disabled_tracker
345 def test_should_not_set_a_disabled_tracker
346 p = Project.find(1)
346 p = Project.find(1)
347 p.trackers.delete(Tracker.find(2))
347 p.trackers.delete(Tracker.find(2))
348
348
349 issue = Issue.find(1)
349 issue = Issue.find(1)
350 issue.tracker_id = 2
350 issue.tracker_id = 2
351 issue.subject = 'New subject'
351 issue.subject = 'New subject'
352 assert !issue.save
352 assert !issue.save
353 assert_not_nil issue.errors[:tracker_id]
353 assert_not_nil issue.errors[:tracker_id]
354 end
354 end
355
355
356 def test_category_based_assignment
356 def test_category_based_assignment
357 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3,
357 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3,
358 :status_id => 1, :priority => IssuePriority.all.first,
358 :status_id => 1, :priority => IssuePriority.all.first,
359 :subject => 'Assignment test',
359 :subject => 'Assignment test',
360 :description => 'Assignment test', :category_id => 1)
360 :description => 'Assignment test', :category_id => 1)
361 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
361 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
362 end
362 end
363
363
364 def test_new_statuses_allowed_to
364 def test_new_statuses_allowed_to
365 Workflow.delete_all
365 Workflow.delete_all
366
366
367 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 2, :author => false, :assignee => false)
367 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 2, :author => false, :assignee => false)
368 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 3, :author => true, :assignee => false)
368 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 3, :author => true, :assignee => false)
369 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 4, :author => false, :assignee => true)
369 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 4, :author => false, :assignee => true)
370 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 5, :author => true, :assignee => true)
370 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 5, :author => true, :assignee => true)
371 status = IssueStatus.find(1)
371 status = IssueStatus.find(1)
372 role = Role.find(1)
372 role = Role.find(1)
373 tracker = Tracker.find(1)
373 tracker = Tracker.find(1)
374 user = User.find(2)
374 user = User.find(2)
375
375
376 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1)
376 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1)
377 assert_equal [1, 2], issue.new_statuses_allowed_to(user).map(&:id)
377 assert_equal [1, 2], issue.new_statuses_allowed_to(user).map(&:id)
378
378
379 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1, :author => user)
379 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1, :author => user)
380 assert_equal [1, 2, 3, 5], issue.new_statuses_allowed_to(user).map(&:id)
380 assert_equal [1, 2, 3, 5], issue.new_statuses_allowed_to(user).map(&:id)
381
381
382 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1, :assigned_to => user)
382 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1, :assigned_to => user)
383 assert_equal [1, 2, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
383 assert_equal [1, 2, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
384
384
385 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1, :author => user, :assigned_to => user)
385 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1, :author => user, :assigned_to => user)
386 assert_equal [1, 2, 3, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
386 assert_equal [1, 2, 3, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
387 end
387 end
388
388
389 def test_new_statuses_allowed_to_should_return_all_transitions_for_admin
389 def test_new_statuses_allowed_to_should_return_all_transitions_for_admin
390 admin = User.find(1)
390 admin = User.find(1)
391 issue = Issue.find(1)
391 issue = Issue.find(1)
392 assert !admin.member_of?(issue.project)
392 assert !admin.member_of?(issue.project)
393 expected_statuses = [issue.status] + Workflow.find_all_by_old_status_id(issue.status_id).map(&:new_status).uniq.sort
393 expected_statuses = [issue.status] + Workflow.find_all_by_old_status_id(issue.status_id).map(&:new_status).uniq.sort
394
394
395 assert_equal expected_statuses, issue.new_statuses_allowed_to(admin)
395 assert_equal expected_statuses, issue.new_statuses_allowed_to(admin)
396 end
396 end
397
397
398 def test_new_statuses_allowed_to_should_return_default_and_current_status_when_copying
398 def test_new_statuses_allowed_to_should_return_default_and_current_status_when_copying
399 issue = Issue.find(1).copy
399 issue = Issue.find(1).copy
400 assert_equal [1], issue.new_statuses_allowed_to(User.find(2)).map(&:id)
400 assert_equal [1], issue.new_statuses_allowed_to(User.find(2)).map(&:id)
401
401
402 issue = Issue.find(2).copy
402 issue = Issue.find(2).copy
403 assert_equal [1, 2], issue.new_statuses_allowed_to(User.find(2)).map(&:id)
403 assert_equal [1, 2], issue.new_statuses_allowed_to(User.find(2)).map(&:id)
404 end
404 end
405
405
406 def test_copy
406 def test_copy
407 issue = Issue.new.copy_from(1)
407 issue = Issue.new.copy_from(1)
408 assert issue.copy?
408 assert issue.copy?
409 assert issue.save
409 assert issue.save
410 issue.reload
410 issue.reload
411 orig = Issue.find(1)
411 orig = Issue.find(1)
412 assert_equal orig.subject, issue.subject
412 assert_equal orig.subject, issue.subject
413 assert_equal orig.tracker, issue.tracker
413 assert_equal orig.tracker, issue.tracker
414 assert_equal "125", issue.custom_value_for(2).value
414 assert_equal "125", issue.custom_value_for(2).value
415 end
415 end
416
416
417 def test_copy_should_copy_status
417 def test_copy_should_copy_status
418 orig = Issue.find(8)
418 orig = Issue.find(8)
419 assert orig.status != IssueStatus.default
419 assert orig.status != IssueStatus.default
420
420
421 issue = Issue.new.copy_from(orig)
421 issue = Issue.new.copy_from(orig)
422 assert issue.save
422 assert issue.save
423 issue.reload
423 issue.reload
424 assert_equal orig.status, issue.status
424 assert_equal orig.status, issue.status
425 end
425 end
426
426
427 def test_should_not_call_after_project_change_on_creation
427 def test_should_not_call_after_project_change_on_creation
428 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1, :subject => 'Test', :author_id => 1)
428 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1, :subject => 'Test', :author_id => 1)
429 issue.expects(:after_project_change).never
429 issue.expects(:after_project_change).never
430 issue.save!
430 issue.save!
431 end
431 end
432
432
433 def test_should_not_call_after_project_change_on_update
433 def test_should_not_call_after_project_change_on_update
434 issue = Issue.find(1)
434 issue = Issue.find(1)
435 issue.project = Project.find(1)
435 issue.project = Project.find(1)
436 issue.subject = 'No project change'
436 issue.subject = 'No project change'
437 issue.expects(:after_project_change).never
437 issue.expects(:after_project_change).never
438 issue.save!
438 issue.save!
439 end
439 end
440
440
441 def test_should_call_after_project_change_on_project_change
441 def test_should_call_after_project_change_on_project_change
442 issue = Issue.find(1)
442 issue = Issue.find(1)
443 issue.project = Project.find(2)
443 issue.project = Project.find(2)
444 issue.expects(:after_project_change).once
444 issue.expects(:after_project_change).once
445 issue.save!
445 issue.save!
446 end
446 end
447
447
448 def test_should_close_duplicates
448 def test_should_close_duplicates
449 # Create 3 issues
449 # Create 3 issues
450 project = Project.find(1)
450 project = Project.find(1)
451 issue1 = Issue.generate_for_project!(project)
451 issue1 = Issue.generate_for_project!(project)
452 issue2 = Issue.generate_for_project!(project)
452 issue2 = Issue.generate_for_project!(project)
453 issue3 = Issue.generate_for_project!(project)
453 issue3 = Issue.generate_for_project!(project)
454
454
455 # 2 is a dupe of 1
455 # 2 is a dupe of 1
456 IssueRelation.create!(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
456 IssueRelation.create!(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
457 # And 3 is a dupe of 2
457 # And 3 is a dupe of 2
458 IssueRelation.create!(:issue_from => issue3, :issue_to => issue2, :relation_type => IssueRelation::TYPE_DUPLICATES)
458 IssueRelation.create!(:issue_from => issue3, :issue_to => issue2, :relation_type => IssueRelation::TYPE_DUPLICATES)
459 # And 3 is a dupe of 1 (circular duplicates)
459 # And 3 is a dupe of 1 (circular duplicates)
460 IssueRelation.create!(:issue_from => issue3, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
460 IssueRelation.create!(:issue_from => issue3, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
461
461
462 assert issue1.reload.duplicates.include?(issue2)
462 assert issue1.reload.duplicates.include?(issue2)
463
463
464 # Closing issue 1
464 # Closing issue 1
465 issue1.init_journal(User.find(:first), "Closing issue1")
465 issue1.init_journal(User.find(:first), "Closing issue1")
466 issue1.status = IssueStatus.find :first, :conditions => {:is_closed => true}
466 issue1.status = IssueStatus.find :first, :conditions => {:is_closed => true}
467 assert issue1.save
467 assert issue1.save
468 # 2 and 3 should be also closed
468 # 2 and 3 should be also closed
469 assert issue2.reload.closed?
469 assert issue2.reload.closed?
470 assert issue3.reload.closed?
470 assert issue3.reload.closed?
471 end
471 end
472
472
473 def test_should_not_close_duplicated_issue
473 def test_should_not_close_duplicated_issue
474 project = Project.find(1)
474 project = Project.find(1)
475 issue1 = Issue.generate_for_project!(project)
475 issue1 = Issue.generate_for_project!(project)
476 issue2 = Issue.generate_for_project!(project)
476 issue2 = Issue.generate_for_project!(project)
477
477
478 # 2 is a dupe of 1
478 # 2 is a dupe of 1
479 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
479 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
480 # 2 is a dup of 1 but 1 is not a duplicate of 2
480 # 2 is a dup of 1 but 1 is not a duplicate of 2
481 assert !issue2.reload.duplicates.include?(issue1)
481 assert !issue2.reload.duplicates.include?(issue1)
482
482
483 # Closing issue 2
483 # Closing issue 2
484 issue2.init_journal(User.find(:first), "Closing issue2")
484 issue2.init_journal(User.find(:first), "Closing issue2")
485 issue2.status = IssueStatus.find :first, :conditions => {:is_closed => true}
485 issue2.status = IssueStatus.find :first, :conditions => {:is_closed => true}
486 assert issue2.save
486 assert issue2.save
487 # 1 should not be also closed
487 # 1 should not be also closed
488 assert !issue1.reload.closed?
488 assert !issue1.reload.closed?
489 end
489 end
490
490
491 def test_assignable_versions
491 def test_assignable_versions
492 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
492 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
493 assert_equal ['open'], issue.assignable_versions.collect(&:status).uniq
493 assert_equal ['open'], issue.assignable_versions.collect(&:status).uniq
494 end
494 end
495
495
496 def test_should_not_be_able_to_assign_a_new_issue_to_a_closed_version
496 def test_should_not_be_able_to_assign_a_new_issue_to_a_closed_version
497 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
497 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
498 assert !issue.save
498 assert !issue.save
499 assert_not_nil issue.errors[:fixed_version_id]
499 assert_not_nil issue.errors[:fixed_version_id]
500 end
500 end
501
501
502 def test_should_not_be_able_to_assign_a_new_issue_to_a_locked_version
502 def test_should_not_be_able_to_assign_a_new_issue_to_a_locked_version
503 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 2, :subject => 'New issue')
503 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 2, :subject => 'New issue')
504 assert !issue.save
504 assert !issue.save
505 assert_not_nil issue.errors[:fixed_version_id]
505 assert_not_nil issue.errors[:fixed_version_id]
506 end
506 end
507
507
508 def test_should_be_able_to_assign_a_new_issue_to_an_open_version
508 def test_should_be_able_to_assign_a_new_issue_to_an_open_version
509 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 3, :subject => 'New issue')
509 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 3, :subject => 'New issue')
510 assert issue.save
510 assert issue.save
511 end
511 end
512
512
513 def test_should_be_able_to_update_an_issue_assigned_to_a_closed_version
513 def test_should_be_able_to_update_an_issue_assigned_to_a_closed_version
514 issue = Issue.find(11)
514 issue = Issue.find(11)
515 assert_equal 'closed', issue.fixed_version.status
515 assert_equal 'closed', issue.fixed_version.status
516 issue.subject = 'Subject changed'
516 issue.subject = 'Subject changed'
517 assert issue.save
517 assert issue.save
518 end
518 end
519
519
520 def test_should_not_be_able_to_reopen_an_issue_assigned_to_a_closed_version
520 def test_should_not_be_able_to_reopen_an_issue_assigned_to_a_closed_version
521 issue = Issue.find(11)
521 issue = Issue.find(11)
522 issue.status_id = 1
522 issue.status_id = 1
523 assert !issue.save
523 assert !issue.save
524 assert_not_nil issue.errors[:base]
524 assert_not_nil issue.errors[:base]
525 end
525 end
526
526
527 def test_should_be_able_to_reopen_and_reassign_an_issue_assigned_to_a_closed_version
527 def test_should_be_able_to_reopen_and_reassign_an_issue_assigned_to_a_closed_version
528 issue = Issue.find(11)
528 issue = Issue.find(11)
529 issue.status_id = 1
529 issue.status_id = 1
530 issue.fixed_version_id = 3
530 issue.fixed_version_id = 3
531 assert issue.save
531 assert issue.save
532 end
532 end
533
533
534 def test_should_be_able_to_reopen_an_issue_assigned_to_a_locked_version
534 def test_should_be_able_to_reopen_an_issue_assigned_to_a_locked_version
535 issue = Issue.find(12)
535 issue = Issue.find(12)
536 assert_equal 'locked', issue.fixed_version.status
536 assert_equal 'locked', issue.fixed_version.status
537 issue.status_id = 1
537 issue.status_id = 1
538 assert issue.save
538 assert issue.save
539 end
539 end
540
540
541 def test_allowed_target_projects_on_move_should_include_projects_with_issue_tracking_enabled
541 def test_allowed_target_projects_on_move_should_include_projects_with_issue_tracking_enabled
542 assert_include Project.find(2), Issue.allowed_target_projects_on_move(User.find(2))
542 assert_include Project.find(2), Issue.allowed_target_projects_on_move(User.find(2))
543 end
543 end
544
544
545 def test_allowed_target_projects_on_move_should_not_include_projects_with_issue_tracking_disabled
545 def test_allowed_target_projects_on_move_should_not_include_projects_with_issue_tracking_disabled
546 Project.find(2).disable_module! :issue_tracking
546 Project.find(2).disable_module! :issue_tracking
547 assert_not_include Project.find(2), Issue.allowed_target_projects_on_move(User.find(2))
547 assert_not_include Project.find(2), Issue.allowed_target_projects_on_move(User.find(2))
548 end
548 end
549
549
550 def test_move_to_another_project_with_same_category
550 def test_move_to_another_project_with_same_category
551 issue = Issue.find(1)
551 issue = Issue.find(1)
552 issue.project = Project.find(2)
552 issue.project = Project.find(2)
553 assert issue.save
553 assert issue.save
554 issue.reload
554 issue.reload
555 assert_equal 2, issue.project_id
555 assert_equal 2, issue.project_id
556 # Category changes
556 # Category changes
557 assert_equal 4, issue.category_id
557 assert_equal 4, issue.category_id
558 # Make sure time entries were move to the target project
558 # Make sure time entries were move to the target project
559 assert_equal 2, issue.time_entries.first.project_id
559 assert_equal 2, issue.time_entries.first.project_id
560 end
560 end
561
561
562 def test_move_to_another_project_without_same_category
562 def test_move_to_another_project_without_same_category
563 issue = Issue.find(2)
563 issue = Issue.find(2)
564 issue.project = Project.find(2)
564 issue.project = Project.find(2)
565 assert issue.save
565 assert issue.save
566 issue.reload
566 issue.reload
567 assert_equal 2, issue.project_id
567 assert_equal 2, issue.project_id
568 # Category cleared
568 # Category cleared
569 assert_nil issue.category_id
569 assert_nil issue.category_id
570 end
570 end
571
571
572 def test_move_to_another_project_should_clear_fixed_version_when_not_shared
572 def test_move_to_another_project_should_clear_fixed_version_when_not_shared
573 issue = Issue.find(1)
573 issue = Issue.find(1)
574 issue.update_attribute(:fixed_version_id, 1)
574 issue.update_attribute(:fixed_version_id, 1)
575 issue.project = Project.find(2)
575 issue.project = Project.find(2)
576 assert issue.save
576 assert issue.save
577 issue.reload
577 issue.reload
578 assert_equal 2, issue.project_id
578 assert_equal 2, issue.project_id
579 # Cleared fixed_version
579 # Cleared fixed_version
580 assert_equal nil, issue.fixed_version
580 assert_equal nil, issue.fixed_version
581 end
581 end
582
582
583 def test_move_to_another_project_should_keep_fixed_version_when_shared_with_the_target_project
583 def test_move_to_another_project_should_keep_fixed_version_when_shared_with_the_target_project
584 issue = Issue.find(1)
584 issue = Issue.find(1)
585 issue.update_attribute(:fixed_version_id, 4)
585 issue.update_attribute(:fixed_version_id, 4)
586 issue.project = Project.find(5)
586 issue.project = Project.find(5)
587 assert issue.save
587 assert issue.save
588 issue.reload
588 issue.reload
589 assert_equal 5, issue.project_id
589 assert_equal 5, issue.project_id
590 # Keep fixed_version
590 # Keep fixed_version
591 assert_equal 4, issue.fixed_version_id
591 assert_equal 4, issue.fixed_version_id
592 end
592 end
593
593
594 def test_move_to_another_project_should_clear_fixed_version_when_not_shared_with_the_target_project
594 def test_move_to_another_project_should_clear_fixed_version_when_not_shared_with_the_target_project
595 issue = Issue.find(1)
595 issue = Issue.find(1)
596 issue.update_attribute(:fixed_version_id, 1)
596 issue.update_attribute(:fixed_version_id, 1)
597 issue.project = Project.find(5)
597 issue.project = Project.find(5)
598 assert issue.save
598 assert issue.save
599 issue.reload
599 issue.reload
600 assert_equal 5, issue.project_id
600 assert_equal 5, issue.project_id
601 # Cleared fixed_version
601 # Cleared fixed_version
602 assert_equal nil, issue.fixed_version
602 assert_equal nil, issue.fixed_version
603 end
603 end
604
604
605 def test_move_to_another_project_should_keep_fixed_version_when_shared_systemwide
605 def test_move_to_another_project_should_keep_fixed_version_when_shared_systemwide
606 issue = Issue.find(1)
606 issue = Issue.find(1)
607 issue.update_attribute(:fixed_version_id, 7)
607 issue.update_attribute(:fixed_version_id, 7)
608 issue.project = Project.find(2)
608 issue.project = Project.find(2)
609 assert issue.save
609 assert issue.save
610 issue.reload
610 issue.reload
611 assert_equal 2, issue.project_id
611 assert_equal 2, issue.project_id
612 # Keep fixed_version
612 # Keep fixed_version
613 assert_equal 7, issue.fixed_version_id
613 assert_equal 7, issue.fixed_version_id
614 end
614 end
615
615
616 def test_move_to_another_project_with_disabled_tracker
616 def test_move_to_another_project_with_disabled_tracker
617 issue = Issue.find(1)
617 issue = Issue.find(1)
618 target = Project.find(2)
618 target = Project.find(2)
619 target.tracker_ids = [3]
619 target.tracker_ids = [3]
620 target.save
620 target.save
621 issue.project = target
621 issue.project = target
622 assert issue.save
622 assert issue.save
623 issue.reload
623 issue.reload
624 assert_equal 2, issue.project_id
624 assert_equal 2, issue.project_id
625 assert_equal 3, issue.tracker_id
625 assert_equal 3, issue.tracker_id
626 end
626 end
627
627
628 def test_copy_to_the_same_project
628 def test_copy_to_the_same_project
629 issue = Issue.find(1)
629 issue = Issue.find(1)
630 copy = issue.copy
630 copy = issue.copy
631 assert_difference 'Issue.count' do
631 assert_difference 'Issue.count' do
632 copy.save!
632 copy.save!
633 end
633 end
634 assert_kind_of Issue, copy
634 assert_kind_of Issue, copy
635 assert_equal issue.project, copy.project
635 assert_equal issue.project, copy.project
636 assert_equal "125", copy.custom_value_for(2).value
636 assert_equal "125", copy.custom_value_for(2).value
637 end
637 end
638
638
639 def test_copy_to_another_project_and_tracker
639 def test_copy_to_another_project_and_tracker
640 issue = Issue.find(1)
640 issue = Issue.find(1)
641 copy = issue.copy(:project_id => 3, :tracker_id => 2)
641 copy = issue.copy(:project_id => 3, :tracker_id => 2)
642 assert_difference 'Issue.count' do
642 assert_difference 'Issue.count' do
643 copy.save!
643 copy.save!
644 end
644 end
645 copy.reload
645 copy.reload
646 assert_kind_of Issue, copy
646 assert_kind_of Issue, copy
647 assert_equal Project.find(3), copy.project
647 assert_equal Project.find(3), copy.project
648 assert_equal Tracker.find(2), copy.tracker
648 assert_equal Tracker.find(2), copy.tracker
649 # Custom field #2 is not associated with target tracker
649 # Custom field #2 is not associated with target tracker
650 assert_nil copy.custom_value_for(2)
650 assert_nil copy.custom_value_for(2)
651 end
651 end
652
652
653 context "#copy" do
653 context "#copy" do
654 setup do
654 setup do
655 @issue = Issue.find(1)
655 @issue = Issue.find(1)
656 end
656 end
657
657
658 should "not create a journal" do
658 should "not create a journal" do
659 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :assigned_to_id => 3)
659 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :assigned_to_id => 3)
660 copy.save!
660 copy.save!
661 assert_equal 0, copy.reload.journals.size
661 assert_equal 0, copy.reload.journals.size
662 end
662 end
663
663
664 should "allow assigned_to changes" do
664 should "allow assigned_to changes" do
665 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :assigned_to_id => 3)
665 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :assigned_to_id => 3)
666 assert_equal 3, copy.assigned_to_id
666 assert_equal 3, copy.assigned_to_id
667 end
667 end
668
668
669 should "allow status changes" do
669 should "allow status changes" do
670 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :status_id => 2)
670 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :status_id => 2)
671 assert_equal 2, copy.status_id
671 assert_equal 2, copy.status_id
672 end
672 end
673
673
674 should "allow start date changes" do
674 should "allow start date changes" do
675 date = Date.today
675 date = Date.today
676 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :start_date => date)
676 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :start_date => date)
677 assert_equal date, copy.start_date
677 assert_equal date, copy.start_date
678 end
678 end
679
679
680 should "allow due date changes" do
680 should "allow due date changes" do
681 date = Date.today
681 date = Date.today
682 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :due_date => date)
682 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :due_date => date)
683 assert_equal date, copy.due_date
683 assert_equal date, copy.due_date
684 end
684 end
685
685
686 should "set current user as author" do
686 should "set current user as author" do
687 User.current = User.find(9)
687 User.current = User.find(9)
688 copy = @issue.copy(:project_id => 3, :tracker_id => 2)
688 copy = @issue.copy(:project_id => 3, :tracker_id => 2)
689 assert_equal User.current, copy.author
689 assert_equal User.current, copy.author
690 end
690 end
691
691
692 should "create a journal with notes" do
692 should "create a journal with notes" do
693 date = Date.today
693 date = Date.today
694 notes = "Notes added when copying"
694 notes = "Notes added when copying"
695 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :start_date => date)
695 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :start_date => date)
696 copy.init_journal(User.current, notes)
696 copy.init_journal(User.current, notes)
697 copy.save!
697 copy.save!
698
698
699 assert_equal 1, copy.journals.size
699 assert_equal 1, copy.journals.size
700 journal = copy.journals.first
700 journal = copy.journals.first
701 assert_equal 0, journal.details.size
701 assert_equal 0, journal.details.size
702 assert_equal notes, journal.notes
702 assert_equal notes, journal.notes
703 end
703 end
704 end
704 end
705
705
706 def test_recipients_should_include_previous_assignee
706 def test_recipients_should_include_previous_assignee
707 user = User.find(3)
707 user = User.find(3)
708 user.members.update_all ["mail_notification = ?", false]
708 user.members.update_all ["mail_notification = ?", false]
709 user.update_attribute :mail_notification, 'only_assigned'
709 user.update_attribute :mail_notification, 'only_assigned'
710
710
711 issue = Issue.find(2)
711 issue = Issue.find(2)
712 issue.assigned_to = nil
712 issue.assigned_to = nil
713 assert_include user.mail, issue.recipients
713 assert_include user.mail, issue.recipients
714 issue.save!
714 issue.save!
715 assert !issue.recipients.include?(user.mail)
715 assert !issue.recipients.include?(user.mail)
716 end
716 end
717
717
718 def test_recipients_should_not_include_users_that_cannot_view_the_issue
718 def test_recipients_should_not_include_users_that_cannot_view_the_issue
719 issue = Issue.find(12)
719 issue = Issue.find(12)
720 assert issue.recipients.include?(issue.author.mail)
720 assert issue.recipients.include?(issue.author.mail)
721 # copy the issue to a private project
721 # copy the issue to a private project
722 copy = issue.copy(:project_id => 5, :tracker_id => 2)
722 copy = issue.copy(:project_id => 5, :tracker_id => 2)
723 # author is not a member of project anymore
723 # author is not a member of project anymore
724 assert !copy.recipients.include?(copy.author.mail)
724 assert !copy.recipients.include?(copy.author.mail)
725 end
725 end
726
726
727 def test_recipients_should_include_the_assigned_group_members
727 def test_recipients_should_include_the_assigned_group_members
728 group_member = User.generate_with_protected!
728 group_member = User.generate_with_protected!
729 group = Group.generate!
729 group = Group.generate!
730 group.users << group_member
730 group.users << group_member
731
731
732 issue = Issue.find(12)
732 issue = Issue.find(12)
733 issue.assigned_to = group
733 issue.assigned_to = group
734 assert issue.recipients.include?(group_member.mail)
734 assert issue.recipients.include?(group_member.mail)
735 end
735 end
736
736
737 def test_watcher_recipients_should_not_include_users_that_cannot_view_the_issue
737 def test_watcher_recipients_should_not_include_users_that_cannot_view_the_issue
738 user = User.find(3)
738 user = User.find(3)
739 issue = Issue.find(9)
739 issue = Issue.find(9)
740 Watcher.create!(:user => user, :watchable => issue)
740 Watcher.create!(:user => user, :watchable => issue)
741 assert issue.watched_by?(user)
741 assert issue.watched_by?(user)
742 assert !issue.watcher_recipients.include?(user.mail)
742 assert !issue.watcher_recipients.include?(user.mail)
743 end
743 end
744
744
745 def test_issue_destroy
745 def test_issue_destroy
746 Issue.find(1).destroy
746 Issue.find(1).destroy
747 assert_nil Issue.find_by_id(1)
747 assert_nil Issue.find_by_id(1)
748 assert_nil TimeEntry.find_by_issue_id(1)
748 assert_nil TimeEntry.find_by_issue_id(1)
749 end
749 end
750
750
751 def test_destroying_a_deleted_issue_should_not_raise_an_error
752 issue = Issue.find(1)
753 Issue.find(1).destroy
754
755 assert_nothing_raised do
756 assert_no_difference 'Issue.count' do
757 issue.destroy
758 end
759 assert issue.destroyed?
760 end
761 end
762
763 def test_destroying_a_stale_issue_should_not_raise_an_error
764 issue = Issue.find(1)
765 Issue.find(1).update_attribute :subject, "Updated"
766
767 assert_nothing_raised do
768 assert_difference 'Issue.count', -1 do
769 issue.destroy
770 end
771 assert issue.destroyed?
772 end
773 end
774
751 def test_blocked
775 def test_blocked
752 blocked_issue = Issue.find(9)
776 blocked_issue = Issue.find(9)
753 blocking_issue = Issue.find(10)
777 blocking_issue = Issue.find(10)
754
778
755 assert blocked_issue.blocked?
779 assert blocked_issue.blocked?
756 assert !blocking_issue.blocked?
780 assert !blocking_issue.blocked?
757 end
781 end
758
782
759 def test_blocked_issues_dont_allow_closed_statuses
783 def test_blocked_issues_dont_allow_closed_statuses
760 blocked_issue = Issue.find(9)
784 blocked_issue = Issue.find(9)
761
785
762 allowed_statuses = blocked_issue.new_statuses_allowed_to(users(:users_002))
786 allowed_statuses = blocked_issue.new_statuses_allowed_to(users(:users_002))
763 assert !allowed_statuses.empty?
787 assert !allowed_statuses.empty?
764 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
788 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
765 assert closed_statuses.empty?
789 assert closed_statuses.empty?
766 end
790 end
767
791
768 def test_unblocked_issues_allow_closed_statuses
792 def test_unblocked_issues_allow_closed_statuses
769 blocking_issue = Issue.find(10)
793 blocking_issue = Issue.find(10)
770
794
771 allowed_statuses = blocking_issue.new_statuses_allowed_to(users(:users_002))
795 allowed_statuses = blocking_issue.new_statuses_allowed_to(users(:users_002))
772 assert !allowed_statuses.empty?
796 assert !allowed_statuses.empty?
773 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
797 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
774 assert !closed_statuses.empty?
798 assert !closed_statuses.empty?
775 end
799 end
776
800
777 def test_rescheduling_an_issue_should_reschedule_following_issue
801 def test_rescheduling_an_issue_should_reschedule_following_issue
778 issue1 = Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => '-', :start_date => Date.today, :due_date => Date.today + 2)
802 issue1 = Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => '-', :start_date => Date.today, :due_date => Date.today + 2)
779 issue2 = Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => '-', :start_date => Date.today, :due_date => Date.today + 2)
803 issue2 = Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => '-', :start_date => Date.today, :due_date => Date.today + 2)
780 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_PRECEDES)
804 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_PRECEDES)
781 assert_equal issue1.due_date + 1, issue2.reload.start_date
805 assert_equal issue1.due_date + 1, issue2.reload.start_date
782
806
783 issue1.due_date = Date.today + 5
807 issue1.due_date = Date.today + 5
784 issue1.save!
808 issue1.save!
785 assert_equal issue1.due_date + 1, issue2.reload.start_date
809 assert_equal issue1.due_date + 1, issue2.reload.start_date
786 end
810 end
787
811
788 def test_rescheduling_a_stale_issue_should_not_raise_an_error
812 def test_rescheduling_a_stale_issue_should_not_raise_an_error
789 stale = Issue.find(1)
813 stale = Issue.find(1)
790 issue = Issue.find(1)
814 issue = Issue.find(1)
791 issue.subject = "Updated"
815 issue.subject = "Updated"
792 issue.save!
816 issue.save!
793
817
794 date = 10.days.from_now.to_date
818 date = 10.days.from_now.to_date
795 assert_nothing_raised do
819 assert_nothing_raised do
796 stale.reschedule_after(date)
820 stale.reschedule_after(date)
797 end
821 end
798 assert_equal date, stale.reload.start_date
822 assert_equal date, stale.reload.start_date
799 end
823 end
800
824
801 def test_overdue
825 def test_overdue
802 assert Issue.new(:due_date => 1.day.ago.to_date).overdue?
826 assert Issue.new(:due_date => 1.day.ago.to_date).overdue?
803 assert !Issue.new(:due_date => Date.today).overdue?
827 assert !Issue.new(:due_date => Date.today).overdue?
804 assert !Issue.new(:due_date => 1.day.from_now.to_date).overdue?
828 assert !Issue.new(:due_date => 1.day.from_now.to_date).overdue?
805 assert !Issue.new(:due_date => nil).overdue?
829 assert !Issue.new(:due_date => nil).overdue?
806 assert !Issue.new(:due_date => 1.day.ago.to_date, :status => IssueStatus.find(:first, :conditions => {:is_closed => true})).overdue?
830 assert !Issue.new(:due_date => 1.day.ago.to_date, :status => IssueStatus.find(:first, :conditions => {:is_closed => true})).overdue?
807 end
831 end
808
832
809 context "#behind_schedule?" do
833 context "#behind_schedule?" do
810 should "be false if the issue has no start_date" do
834 should "be false if the issue has no start_date" do
811 assert !Issue.new(:start_date => nil, :due_date => 1.day.from_now.to_date, :done_ratio => 0).behind_schedule?
835 assert !Issue.new(:start_date => nil, :due_date => 1.day.from_now.to_date, :done_ratio => 0).behind_schedule?
812 end
836 end
813
837
814 should "be false if the issue has no end_date" do
838 should "be false if the issue has no end_date" do
815 assert !Issue.new(:start_date => 1.day.from_now.to_date, :due_date => nil, :done_ratio => 0).behind_schedule?
839 assert !Issue.new(:start_date => 1.day.from_now.to_date, :due_date => nil, :done_ratio => 0).behind_schedule?
816 end
840 end
817
841
818 should "be false if the issue has more done than it's calendar time" do
842 should "be false if the issue has more done than it's calendar time" do
819 assert !Issue.new(:start_date => 50.days.ago.to_date, :due_date => 50.days.from_now.to_date, :done_ratio => 90).behind_schedule?
843 assert !Issue.new(:start_date => 50.days.ago.to_date, :due_date => 50.days.from_now.to_date, :done_ratio => 90).behind_schedule?
820 end
844 end
821
845
822 should "be true if the issue hasn't been started at all" do
846 should "be true if the issue hasn't been started at all" do
823 assert Issue.new(:start_date => 1.day.ago.to_date, :due_date => 1.day.from_now.to_date, :done_ratio => 0).behind_schedule?
847 assert Issue.new(:start_date => 1.day.ago.to_date, :due_date => 1.day.from_now.to_date, :done_ratio => 0).behind_schedule?
824 end
848 end
825
849
826 should "be true if the issue has used more calendar time than it's done ratio" do
850 should "be true if the issue has used more calendar time than it's done ratio" do
827 assert Issue.new(:start_date => 100.days.ago.to_date, :due_date => Date.today, :done_ratio => 90).behind_schedule?
851 assert Issue.new(:start_date => 100.days.ago.to_date, :due_date => Date.today, :done_ratio => 90).behind_schedule?
828 end
852 end
829 end
853 end
830
854
831 context "#assignable_users" do
855 context "#assignable_users" do
832 should "be Users" do
856 should "be Users" do
833 assert_kind_of User, Issue.find(1).assignable_users.first
857 assert_kind_of User, Issue.find(1).assignable_users.first
834 end
858 end
835
859
836 should "include the issue author" do
860 should "include the issue author" do
837 project = Project.find(1)
861 project = Project.find(1)
838 non_project_member = User.generate!
862 non_project_member = User.generate!
839 issue = Issue.generate_for_project!(project, :author => non_project_member)
863 issue = Issue.generate_for_project!(project, :author => non_project_member)
840
864
841 assert issue.assignable_users.include?(non_project_member)
865 assert issue.assignable_users.include?(non_project_member)
842 end
866 end
843
867
844 should "include the current assignee" do
868 should "include the current assignee" do
845 project = Project.find(1)
869 project = Project.find(1)
846 user = User.generate!
870 user = User.generate!
847 issue = Issue.generate_for_project!(project, :assigned_to => user)
871 issue = Issue.generate_for_project!(project, :assigned_to => user)
848 user.lock!
872 user.lock!
849
873
850 assert Issue.find(issue.id).assignable_users.include?(user)
874 assert Issue.find(issue.id).assignable_users.include?(user)
851 end
875 end
852
876
853 should "not show the issue author twice" do
877 should "not show the issue author twice" do
854 assignable_user_ids = Issue.find(1).assignable_users.collect(&:id)
878 assignable_user_ids = Issue.find(1).assignable_users.collect(&:id)
855 assert_equal 2, assignable_user_ids.length
879 assert_equal 2, assignable_user_ids.length
856
880
857 assignable_user_ids.each do |user_id|
881 assignable_user_ids.each do |user_id|
858 assert_equal 1, assignable_user_ids.select {|i| i == user_id}.length, "User #{user_id} appears more or less than once"
882 assert_equal 1, assignable_user_ids.select {|i| i == user_id}.length, "User #{user_id} appears more or less than once"
859 end
883 end
860 end
884 end
861
885
862 context "with issue_group_assignment" do
886 context "with issue_group_assignment" do
863 should "include groups" do
887 should "include groups" do
864 issue = Issue.new(:project => Project.find(2))
888 issue = Issue.new(:project => Project.find(2))
865
889
866 with_settings :issue_group_assignment => '1' do
890 with_settings :issue_group_assignment => '1' do
867 assert_equal %w(Group User), issue.assignable_users.map {|a| a.class.name}.uniq.sort
891 assert_equal %w(Group User), issue.assignable_users.map {|a| a.class.name}.uniq.sort
868 assert issue.assignable_users.include?(Group.find(11))
892 assert issue.assignable_users.include?(Group.find(11))
869 end
893 end
870 end
894 end
871 end
895 end
872
896
873 context "without issue_group_assignment" do
897 context "without issue_group_assignment" do
874 should "not include groups" do
898 should "not include groups" do
875 issue = Issue.new(:project => Project.find(2))
899 issue = Issue.new(:project => Project.find(2))
876
900
877 with_settings :issue_group_assignment => '0' do
901 with_settings :issue_group_assignment => '0' do
878 assert_equal %w(User), issue.assignable_users.map {|a| a.class.name}.uniq.sort
902 assert_equal %w(User), issue.assignable_users.map {|a| a.class.name}.uniq.sort
879 assert !issue.assignable_users.include?(Group.find(11))
903 assert !issue.assignable_users.include?(Group.find(11))
880 end
904 end
881 end
905 end
882 end
906 end
883 end
907 end
884
908
885 def test_create_should_send_email_notification
909 def test_create_should_send_email_notification
886 ActionMailer::Base.deliveries.clear
910 ActionMailer::Base.deliveries.clear
887 issue = Issue.new(:project_id => 1, :tracker_id => 1,
911 issue = Issue.new(:project_id => 1, :tracker_id => 1,
888 :author_id => 3, :status_id => 1,
912 :author_id => 3, :status_id => 1,
889 :priority => IssuePriority.all.first,
913 :priority => IssuePriority.all.first,
890 :subject => 'test_create', :estimated_hours => '1:30')
914 :subject => 'test_create', :estimated_hours => '1:30')
891
915
892 assert issue.save
916 assert issue.save
893 assert_equal 1, ActionMailer::Base.deliveries.size
917 assert_equal 1, ActionMailer::Base.deliveries.size
894 end
918 end
895
919
896 def test_stale_issue_should_not_send_email_notification
920 def test_stale_issue_should_not_send_email_notification
897 ActionMailer::Base.deliveries.clear
921 ActionMailer::Base.deliveries.clear
898 issue = Issue.find(1)
922 issue = Issue.find(1)
899 stale = Issue.find(1)
923 stale = Issue.find(1)
900
924
901 issue.init_journal(User.find(1))
925 issue.init_journal(User.find(1))
902 issue.subject = 'Subjet update'
926 issue.subject = 'Subjet update'
903 assert issue.save
927 assert issue.save
904 assert_equal 1, ActionMailer::Base.deliveries.size
928 assert_equal 1, ActionMailer::Base.deliveries.size
905 ActionMailer::Base.deliveries.clear
929 ActionMailer::Base.deliveries.clear
906
930
907 stale.init_journal(User.find(1))
931 stale.init_journal(User.find(1))
908 stale.subject = 'Another subjet update'
932 stale.subject = 'Another subjet update'
909 assert_raise ActiveRecord::StaleObjectError do
933 assert_raise ActiveRecord::StaleObjectError do
910 stale.save
934 stale.save
911 end
935 end
912 assert ActionMailer::Base.deliveries.empty?
936 assert ActionMailer::Base.deliveries.empty?
913 end
937 end
914
938
915 def test_journalized_description
939 def test_journalized_description
916 IssueCustomField.delete_all
940 IssueCustomField.delete_all
917
941
918 i = Issue.first
942 i = Issue.first
919 old_description = i.description
943 old_description = i.description
920 new_description = "This is the new description"
944 new_description = "This is the new description"
921
945
922 i.init_journal(User.find(2))
946 i.init_journal(User.find(2))
923 i.description = new_description
947 i.description = new_description
924 assert_difference 'Journal.count', 1 do
948 assert_difference 'Journal.count', 1 do
925 assert_difference 'JournalDetail.count', 1 do
949 assert_difference 'JournalDetail.count', 1 do
926 i.save!
950 i.save!
927 end
951 end
928 end
952 end
929
953
930 detail = JournalDetail.first(:order => 'id DESC')
954 detail = JournalDetail.first(:order => 'id DESC')
931 assert_equal i, detail.journal.journalized
955 assert_equal i, detail.journal.journalized
932 assert_equal 'attr', detail.property
956 assert_equal 'attr', detail.property
933 assert_equal 'description', detail.prop_key
957 assert_equal 'description', detail.prop_key
934 assert_equal old_description, detail.old_value
958 assert_equal old_description, detail.old_value
935 assert_equal new_description, detail.value
959 assert_equal new_description, detail.value
936 end
960 end
937
961
938 def test_blank_descriptions_should_not_be_journalized
962 def test_blank_descriptions_should_not_be_journalized
939 IssueCustomField.delete_all
963 IssueCustomField.delete_all
940 Issue.update_all("description = NULL", "id=1")
964 Issue.update_all("description = NULL", "id=1")
941
965
942 i = Issue.find(1)
966 i = Issue.find(1)
943 i.init_journal(User.find(2))
967 i.init_journal(User.find(2))
944 i.subject = "blank description"
968 i.subject = "blank description"
945 i.description = "\r\n"
969 i.description = "\r\n"
946
970
947 assert_difference 'Journal.count', 1 do
971 assert_difference 'Journal.count', 1 do
948 assert_difference 'JournalDetail.count', 1 do
972 assert_difference 'JournalDetail.count', 1 do
949 i.save!
973 i.save!
950 end
974 end
951 end
975 end
952 end
976 end
953
977
954 def test_journalized_multi_custom_field
978 def test_journalized_multi_custom_field
955 field = IssueCustomField.create!(:name => 'filter', :field_format => 'list', :is_filter => true, :is_for_all => true,
979 field = IssueCustomField.create!(:name => 'filter', :field_format => 'list', :is_filter => true, :is_for_all => true,
956 :tracker_ids => [1], :possible_values => ['value1', 'value2', 'value3'], :multiple => true)
980 :tracker_ids => [1], :possible_values => ['value1', 'value2', 'value3'], :multiple => true)
957
981
958 issue = Issue.create!(:project_id => 1, :tracker_id => 1, :subject => 'Test', :author_id => 1)
982 issue = Issue.create!(:project_id => 1, :tracker_id => 1, :subject => 'Test', :author_id => 1)
959
983
960 assert_difference 'Journal.count' do
984 assert_difference 'Journal.count' do
961 assert_difference 'JournalDetail.count' do
985 assert_difference 'JournalDetail.count' do
962 issue.init_journal(User.first)
986 issue.init_journal(User.first)
963 issue.custom_field_values = {field.id => ['value1']}
987 issue.custom_field_values = {field.id => ['value1']}
964 issue.save!
988 issue.save!
965 end
989 end
966 assert_difference 'JournalDetail.count' do
990 assert_difference 'JournalDetail.count' do
967 issue.init_journal(User.first)
991 issue.init_journal(User.first)
968 issue.custom_field_values = {field.id => ['value1', 'value2']}
992 issue.custom_field_values = {field.id => ['value1', 'value2']}
969 issue.save!
993 issue.save!
970 end
994 end
971 assert_difference 'JournalDetail.count', 2 do
995 assert_difference 'JournalDetail.count', 2 do
972 issue.init_journal(User.first)
996 issue.init_journal(User.first)
973 issue.custom_field_values = {field.id => ['value3', 'value2']}
997 issue.custom_field_values = {field.id => ['value3', 'value2']}
974 issue.save!
998 issue.save!
975 end
999 end
976 assert_difference 'JournalDetail.count', 2 do
1000 assert_difference 'JournalDetail.count', 2 do
977 issue.init_journal(User.first)
1001 issue.init_journal(User.first)
978 issue.custom_field_values = {field.id => nil}
1002 issue.custom_field_values = {field.id => nil}
979 issue.save!
1003 issue.save!
980 end
1004 end
981 end
1005 end
982 end
1006 end
983
1007
984 def test_description_eol_should_be_normalized
1008 def test_description_eol_should_be_normalized
985 i = Issue.new(:description => "CR \r LF \n CRLF \r\n")
1009 i = Issue.new(:description => "CR \r LF \n CRLF \r\n")
986 assert_equal "CR \r\n LF \r\n CRLF \r\n", i.description
1010 assert_equal "CR \r\n LF \r\n CRLF \r\n", i.description
987 end
1011 end
988
1012
989 def test_saving_twice_should_not_duplicate_journal_details
1013 def test_saving_twice_should_not_duplicate_journal_details
990 i = Issue.find(:first)
1014 i = Issue.find(:first)
991 i.init_journal(User.find(2), 'Some notes')
1015 i.init_journal(User.find(2), 'Some notes')
992 # initial changes
1016 # initial changes
993 i.subject = 'New subject'
1017 i.subject = 'New subject'
994 i.done_ratio = i.done_ratio + 10
1018 i.done_ratio = i.done_ratio + 10
995 assert_difference 'Journal.count' do
1019 assert_difference 'Journal.count' do
996 assert i.save
1020 assert i.save
997 end
1021 end
998 # 1 more change
1022 # 1 more change
999 i.priority = IssuePriority.find(:first, :conditions => ["id <> ?", i.priority_id])
1023 i.priority = IssuePriority.find(:first, :conditions => ["id <> ?", i.priority_id])
1000 assert_no_difference 'Journal.count' do
1024 assert_no_difference 'Journal.count' do
1001 assert_difference 'JournalDetail.count', 1 do
1025 assert_difference 'JournalDetail.count', 1 do
1002 i.save
1026 i.save
1003 end
1027 end
1004 end
1028 end
1005 # no more change
1029 # no more change
1006 assert_no_difference 'Journal.count' do
1030 assert_no_difference 'Journal.count' do
1007 assert_no_difference 'JournalDetail.count' do
1031 assert_no_difference 'JournalDetail.count' do
1008 i.save
1032 i.save
1009 end
1033 end
1010 end
1034 end
1011 end
1035 end
1012
1036
1013 def test_all_dependent_issues
1037 def test_all_dependent_issues
1014 IssueRelation.delete_all
1038 IssueRelation.delete_all
1015 assert IssueRelation.create!(:issue_from => Issue.find(1),
1039 assert IssueRelation.create!(:issue_from => Issue.find(1),
1016 :issue_to => Issue.find(2),
1040 :issue_to => Issue.find(2),
1017 :relation_type => IssueRelation::TYPE_PRECEDES)
1041 :relation_type => IssueRelation::TYPE_PRECEDES)
1018 assert IssueRelation.create!(:issue_from => Issue.find(2),
1042 assert IssueRelation.create!(:issue_from => Issue.find(2),
1019 :issue_to => Issue.find(3),
1043 :issue_to => Issue.find(3),
1020 :relation_type => IssueRelation::TYPE_PRECEDES)
1044 :relation_type => IssueRelation::TYPE_PRECEDES)
1021 assert IssueRelation.create!(:issue_from => Issue.find(3),
1045 assert IssueRelation.create!(:issue_from => Issue.find(3),
1022 :issue_to => Issue.find(8),
1046 :issue_to => Issue.find(8),
1023 :relation_type => IssueRelation::TYPE_PRECEDES)
1047 :relation_type => IssueRelation::TYPE_PRECEDES)
1024
1048
1025 assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort
1049 assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort
1026 end
1050 end
1027
1051
1028 def test_all_dependent_issues_with_persistent_circular_dependency
1052 def test_all_dependent_issues_with_persistent_circular_dependency
1029 IssueRelation.delete_all
1053 IssueRelation.delete_all
1030 assert IssueRelation.create!(:issue_from => Issue.find(1),
1054 assert IssueRelation.create!(:issue_from => Issue.find(1),
1031 :issue_to => Issue.find(2),
1055 :issue_to => Issue.find(2),
1032 :relation_type => IssueRelation::TYPE_PRECEDES)
1056 :relation_type => IssueRelation::TYPE_PRECEDES)
1033 assert IssueRelation.create!(:issue_from => Issue.find(2),
1057 assert IssueRelation.create!(:issue_from => Issue.find(2),
1034 :issue_to => Issue.find(3),
1058 :issue_to => Issue.find(3),
1035 :relation_type => IssueRelation::TYPE_PRECEDES)
1059 :relation_type => IssueRelation::TYPE_PRECEDES)
1036 # Validation skipping
1060 # Validation skipping
1037 assert IssueRelation.new(:issue_from => Issue.find(3),
1061 assert IssueRelation.new(:issue_from => Issue.find(3),
1038 :issue_to => Issue.find(1),
1062 :issue_to => Issue.find(1),
1039 :relation_type => IssueRelation::TYPE_PRECEDES).save(false)
1063 :relation_type => IssueRelation::TYPE_PRECEDES).save(false)
1040
1064
1041 assert_equal [2, 3], Issue.find(1).all_dependent_issues.collect(&:id).sort
1065 assert_equal [2, 3], Issue.find(1).all_dependent_issues.collect(&:id).sort
1042 end
1066 end
1043
1067
1044 def test_all_dependent_issues_with_persistent_multiple_circular_dependencies
1068 def test_all_dependent_issues_with_persistent_multiple_circular_dependencies
1045 IssueRelation.delete_all
1069 IssueRelation.delete_all
1046 assert IssueRelation.create!(:issue_from => Issue.find(1),
1070 assert IssueRelation.create!(:issue_from => Issue.find(1),
1047 :issue_to => Issue.find(2),
1071 :issue_to => Issue.find(2),
1048 :relation_type => IssueRelation::TYPE_RELATES)
1072 :relation_type => IssueRelation::TYPE_RELATES)
1049 assert IssueRelation.create!(:issue_from => Issue.find(2),
1073 assert IssueRelation.create!(:issue_from => Issue.find(2),
1050 :issue_to => Issue.find(3),
1074 :issue_to => Issue.find(3),
1051 :relation_type => IssueRelation::TYPE_RELATES)
1075 :relation_type => IssueRelation::TYPE_RELATES)
1052 assert IssueRelation.create!(:issue_from => Issue.find(3),
1076 assert IssueRelation.create!(:issue_from => Issue.find(3),
1053 :issue_to => Issue.find(8),
1077 :issue_to => Issue.find(8),
1054 :relation_type => IssueRelation::TYPE_RELATES)
1078 :relation_type => IssueRelation::TYPE_RELATES)
1055 # Validation skipping
1079 # Validation skipping
1056 assert IssueRelation.new(:issue_from => Issue.find(8),
1080 assert IssueRelation.new(:issue_from => Issue.find(8),
1057 :issue_to => Issue.find(2),
1081 :issue_to => Issue.find(2),
1058 :relation_type => IssueRelation::TYPE_RELATES).save(false)
1082 :relation_type => IssueRelation::TYPE_RELATES).save(false)
1059 assert IssueRelation.new(:issue_from => Issue.find(3),
1083 assert IssueRelation.new(:issue_from => Issue.find(3),
1060 :issue_to => Issue.find(1),
1084 :issue_to => Issue.find(1),
1061 :relation_type => IssueRelation::TYPE_RELATES).save(false)
1085 :relation_type => IssueRelation::TYPE_RELATES).save(false)
1062
1086
1063 assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort
1087 assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort
1064 end
1088 end
1065
1089
1066 context "#done_ratio" do
1090 context "#done_ratio" do
1067 setup do
1091 setup do
1068 @issue = Issue.find(1)
1092 @issue = Issue.find(1)
1069 @issue_status = IssueStatus.find(1)
1093 @issue_status = IssueStatus.find(1)
1070 @issue_status.update_attribute(:default_done_ratio, 50)
1094 @issue_status.update_attribute(:default_done_ratio, 50)
1071 @issue2 = Issue.find(2)
1095 @issue2 = Issue.find(2)
1072 @issue_status2 = IssueStatus.find(2)
1096 @issue_status2 = IssueStatus.find(2)
1073 @issue_status2.update_attribute(:default_done_ratio, 0)
1097 @issue_status2.update_attribute(:default_done_ratio, 0)
1074 end
1098 end
1075
1099
1076 teardown do
1100 teardown do
1077 Setting.issue_done_ratio = 'issue_field'
1101 Setting.issue_done_ratio = 'issue_field'
1078 end
1102 end
1079
1103
1080 context "with Setting.issue_done_ratio using the issue_field" do
1104 context "with Setting.issue_done_ratio using the issue_field" do
1081 setup do
1105 setup do
1082 Setting.issue_done_ratio = 'issue_field'
1106 Setting.issue_done_ratio = 'issue_field'
1083 end
1107 end
1084
1108
1085 should "read the issue's field" do
1109 should "read the issue's field" do
1086 assert_equal 0, @issue.done_ratio
1110 assert_equal 0, @issue.done_ratio
1087 assert_equal 30, @issue2.done_ratio
1111 assert_equal 30, @issue2.done_ratio
1088 end
1112 end
1089 end
1113 end
1090
1114
1091 context "with Setting.issue_done_ratio using the issue_status" do
1115 context "with Setting.issue_done_ratio using the issue_status" do
1092 setup do
1116 setup do
1093 Setting.issue_done_ratio = 'issue_status'
1117 Setting.issue_done_ratio = 'issue_status'
1094 end
1118 end
1095
1119
1096 should "read the Issue Status's default done ratio" do
1120 should "read the Issue Status's default done ratio" do
1097 assert_equal 50, @issue.done_ratio
1121 assert_equal 50, @issue.done_ratio
1098 assert_equal 0, @issue2.done_ratio
1122 assert_equal 0, @issue2.done_ratio
1099 end
1123 end
1100 end
1124 end
1101 end
1125 end
1102
1126
1103 context "#update_done_ratio_from_issue_status" do
1127 context "#update_done_ratio_from_issue_status" do
1104 setup do
1128 setup do
1105 @issue = Issue.find(1)
1129 @issue = Issue.find(1)
1106 @issue_status = IssueStatus.find(1)
1130 @issue_status = IssueStatus.find(1)
1107 @issue_status.update_attribute(:default_done_ratio, 50)
1131 @issue_status.update_attribute(:default_done_ratio, 50)
1108 @issue2 = Issue.find(2)
1132 @issue2 = Issue.find(2)
1109 @issue_status2 = IssueStatus.find(2)
1133 @issue_status2 = IssueStatus.find(2)
1110 @issue_status2.update_attribute(:default_done_ratio, 0)
1134 @issue_status2.update_attribute(:default_done_ratio, 0)
1111 end
1135 end
1112
1136
1113 context "with Setting.issue_done_ratio using the issue_field" do
1137 context "with Setting.issue_done_ratio using the issue_field" do
1114 setup do
1138 setup do
1115 Setting.issue_done_ratio = 'issue_field'
1139 Setting.issue_done_ratio = 'issue_field'
1116 end
1140 end
1117
1141
1118 should "not change the issue" do
1142 should "not change the issue" do
1119 @issue.update_done_ratio_from_issue_status
1143 @issue.update_done_ratio_from_issue_status
1120 @issue2.update_done_ratio_from_issue_status
1144 @issue2.update_done_ratio_from_issue_status
1121
1145
1122 assert_equal 0, @issue.read_attribute(:done_ratio)
1146 assert_equal 0, @issue.read_attribute(:done_ratio)
1123 assert_equal 30, @issue2.read_attribute(:done_ratio)
1147 assert_equal 30, @issue2.read_attribute(:done_ratio)
1124 end
1148 end
1125 end
1149 end
1126
1150
1127 context "with Setting.issue_done_ratio using the issue_status" do
1151 context "with Setting.issue_done_ratio using the issue_status" do
1128 setup do
1152 setup do
1129 Setting.issue_done_ratio = 'issue_status'
1153 Setting.issue_done_ratio = 'issue_status'
1130 end
1154 end
1131
1155
1132 should "change the issue's done ratio" do
1156 should "change the issue's done ratio" do
1133 @issue.update_done_ratio_from_issue_status
1157 @issue.update_done_ratio_from_issue_status
1134 @issue2.update_done_ratio_from_issue_status
1158 @issue2.update_done_ratio_from_issue_status
1135
1159
1136 assert_equal 50, @issue.read_attribute(:done_ratio)
1160 assert_equal 50, @issue.read_attribute(:done_ratio)
1137 assert_equal 0, @issue2.read_attribute(:done_ratio)
1161 assert_equal 0, @issue2.read_attribute(:done_ratio)
1138 end
1162 end
1139 end
1163 end
1140 end
1164 end
1141
1165
1142 test "#by_tracker" do
1166 test "#by_tracker" do
1143 User.current = User.anonymous
1167 User.current = User.anonymous
1144 groups = Issue.by_tracker(Project.find(1))
1168 groups = Issue.by_tracker(Project.find(1))
1145 assert_equal 3, groups.size
1169 assert_equal 3, groups.size
1146 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1170 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1147 end
1171 end
1148
1172
1149 test "#by_version" do
1173 test "#by_version" do
1150 User.current = User.anonymous
1174 User.current = User.anonymous
1151 groups = Issue.by_version(Project.find(1))
1175 groups = Issue.by_version(Project.find(1))
1152 assert_equal 3, groups.size
1176 assert_equal 3, groups.size
1153 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1177 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1154 end
1178 end
1155
1179
1156 test "#by_priority" do
1180 test "#by_priority" do
1157 User.current = User.anonymous
1181 User.current = User.anonymous
1158 groups = Issue.by_priority(Project.find(1))
1182 groups = Issue.by_priority(Project.find(1))
1159 assert_equal 4, groups.size
1183 assert_equal 4, groups.size
1160 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1184 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1161 end
1185 end
1162
1186
1163 test "#by_category" do
1187 test "#by_category" do
1164 User.current = User.anonymous
1188 User.current = User.anonymous
1165 groups = Issue.by_category(Project.find(1))
1189 groups = Issue.by_category(Project.find(1))
1166 assert_equal 2, groups.size
1190 assert_equal 2, groups.size
1167 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1191 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1168 end
1192 end
1169
1193
1170 test "#by_assigned_to" do
1194 test "#by_assigned_to" do
1171 User.current = User.anonymous
1195 User.current = User.anonymous
1172 groups = Issue.by_assigned_to(Project.find(1))
1196 groups = Issue.by_assigned_to(Project.find(1))
1173 assert_equal 2, groups.size
1197 assert_equal 2, groups.size
1174 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1198 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1175 end
1199 end
1176
1200
1177 test "#by_author" do
1201 test "#by_author" do
1178 User.current = User.anonymous
1202 User.current = User.anonymous
1179 groups = Issue.by_author(Project.find(1))
1203 groups = Issue.by_author(Project.find(1))
1180 assert_equal 4, groups.size
1204 assert_equal 4, groups.size
1181 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1205 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1182 end
1206 end
1183
1207
1184 test "#by_subproject" do
1208 test "#by_subproject" do
1185 User.current = User.anonymous
1209 User.current = User.anonymous
1186 groups = Issue.by_subproject(Project.find(1))
1210 groups = Issue.by_subproject(Project.find(1))
1187 # Private descendant not visible
1211 # Private descendant not visible
1188 assert_equal 1, groups.size
1212 assert_equal 1, groups.size
1189 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1213 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1190 end
1214 end
1191
1215
1192 def test_recently_updated_with_limit_scopes
1216 def test_recently_updated_with_limit_scopes
1193 #should return the last updated issue
1217 #should return the last updated issue
1194 assert_equal 1, Issue.recently_updated.with_limit(1).length
1218 assert_equal 1, Issue.recently_updated.with_limit(1).length
1195 assert_equal Issue.find(:first, :order => "updated_on DESC"), Issue.recently_updated.with_limit(1).first
1219 assert_equal Issue.find(:first, :order => "updated_on DESC"), Issue.recently_updated.with_limit(1).first
1196 end
1220 end
1197
1221
1198 def test_on_active_projects_scope
1222 def test_on_active_projects_scope
1199 assert Project.find(2).archive
1223 assert Project.find(2).archive
1200
1224
1201 before = Issue.on_active_project.length
1225 before = Issue.on_active_project.length
1202 # test inclusion to results
1226 # test inclusion to results
1203 issue = Issue.generate_for_project!(Project.find(1), :tracker => Project.find(2).trackers.first)
1227 issue = Issue.generate_for_project!(Project.find(1), :tracker => Project.find(2).trackers.first)
1204 assert_equal before + 1, Issue.on_active_project.length
1228 assert_equal before + 1, Issue.on_active_project.length
1205
1229
1206 # Move to an archived project
1230 # Move to an archived project
1207 issue.project = Project.find(2)
1231 issue.project = Project.find(2)
1208 assert issue.save
1232 assert issue.save
1209 assert_equal before, Issue.on_active_project.length
1233 assert_equal before, Issue.on_active_project.length
1210 end
1234 end
1211
1235
1212 context "Issue#recipients" do
1236 context "Issue#recipients" do
1213 setup do
1237 setup do
1214 @project = Project.find(1)
1238 @project = Project.find(1)
1215 @author = User.generate_with_protected!
1239 @author = User.generate_with_protected!
1216 @assignee = User.generate_with_protected!
1240 @assignee = User.generate_with_protected!
1217 @issue = Issue.generate_for_project!(@project, :assigned_to => @assignee, :author => @author)
1241 @issue = Issue.generate_for_project!(@project, :assigned_to => @assignee, :author => @author)
1218 end
1242 end
1219
1243
1220 should "include project recipients" do
1244 should "include project recipients" do
1221 assert @project.recipients.present?
1245 assert @project.recipients.present?
1222 @project.recipients.each do |project_recipient|
1246 @project.recipients.each do |project_recipient|
1223 assert @issue.recipients.include?(project_recipient)
1247 assert @issue.recipients.include?(project_recipient)
1224 end
1248 end
1225 end
1249 end
1226
1250
1227 should "include the author if the author is active" do
1251 should "include the author if the author is active" do
1228 assert @issue.author, "No author set for Issue"
1252 assert @issue.author, "No author set for Issue"
1229 assert @issue.recipients.include?(@issue.author.mail)
1253 assert @issue.recipients.include?(@issue.author.mail)
1230 end
1254 end
1231
1255
1232 should "include the assigned to user if the assigned to user is active" do
1256 should "include the assigned to user if the assigned to user is active" do
1233 assert @issue.assigned_to, "No assigned_to set for Issue"
1257 assert @issue.assigned_to, "No assigned_to set for Issue"
1234 assert @issue.recipients.include?(@issue.assigned_to.mail)
1258 assert @issue.recipients.include?(@issue.assigned_to.mail)
1235 end
1259 end
1236
1260
1237 should "not include users who opt out of all email" do
1261 should "not include users who opt out of all email" do
1238 @author.update_attribute(:mail_notification, :none)
1262 @author.update_attribute(:mail_notification, :none)
1239
1263
1240 assert !@issue.recipients.include?(@issue.author.mail)
1264 assert !@issue.recipients.include?(@issue.author.mail)
1241 end
1265 end
1242
1266
1243 should "not include the issue author if they are only notified of assigned issues" do
1267 should "not include the issue author if they are only notified of assigned issues" do
1244 @author.update_attribute(:mail_notification, :only_assigned)
1268 @author.update_attribute(:mail_notification, :only_assigned)
1245
1269
1246 assert !@issue.recipients.include?(@issue.author.mail)
1270 assert !@issue.recipients.include?(@issue.author.mail)
1247 end
1271 end
1248
1272
1249 should "not include the assigned user if they are only notified of owned issues" do
1273 should "not include the assigned user if they are only notified of owned issues" do
1250 @assignee.update_attribute(:mail_notification, :only_owner)
1274 @assignee.update_attribute(:mail_notification, :only_owner)
1251
1275
1252 assert !@issue.recipients.include?(@issue.assigned_to.mail)
1276 assert !@issue.recipients.include?(@issue.assigned_to.mail)
1253 end
1277 end
1254 end
1278 end
1255
1279
1256 def test_last_journal_id_with_journals_should_return_the_journal_id
1280 def test_last_journal_id_with_journals_should_return_the_journal_id
1257 assert_equal 2, Issue.find(1).last_journal_id
1281 assert_equal 2, Issue.find(1).last_journal_id
1258 end
1282 end
1259
1283
1260 def test_last_journal_id_without_journals_should_return_nil
1284 def test_last_journal_id_without_journals_should_return_nil
1261 assert_nil Issue.find(3).last_journal_id
1285 assert_nil Issue.find(3).last_journal_id
1262 end
1286 end
1263 end
1287 end
@@ -1,1150 +1,1162
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 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.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19
19
20 class ProjectTest < ActiveSupport::TestCase
20 class ProjectTest < ActiveSupport::TestCase
21 fixtures :projects, :trackers, :issue_statuses, :issues,
21 fixtures :projects, :trackers, :issue_statuses, :issues,
22 :journals, :journal_details,
22 :journals, :journal_details,
23 :enumerations, :users, :issue_categories,
23 :enumerations, :users, :issue_categories,
24 :projects_trackers,
24 :projects_trackers,
25 :custom_fields,
25 :custom_fields,
26 :custom_fields_projects,
26 :custom_fields_projects,
27 :custom_fields_trackers,
27 :custom_fields_trackers,
28 :custom_values,
28 :custom_values,
29 :roles,
29 :roles,
30 :member_roles,
30 :member_roles,
31 :members,
31 :members,
32 :enabled_modules,
32 :enabled_modules,
33 :workflows,
33 :workflows,
34 :versions,
34 :versions,
35 :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions,
35 :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions,
36 :groups_users,
36 :groups_users,
37 :boards,
37 :boards,
38 :repositories
38 :repositories
39
39
40 def setup
40 def setup
41 @ecookbook = Project.find(1)
41 @ecookbook = Project.find(1)
42 @ecookbook_sub1 = Project.find(3)
42 @ecookbook_sub1 = Project.find(3)
43 set_tmp_attachments_directory
43 set_tmp_attachments_directory
44 User.current = nil
44 User.current = nil
45 end
45 end
46
46
47 def test_truth
47 def test_truth
48 assert_kind_of Project, @ecookbook
48 assert_kind_of Project, @ecookbook
49 assert_equal "eCookbook", @ecookbook.name
49 assert_equal "eCookbook", @ecookbook.name
50 end
50 end
51
51
52 def test_default_attributes
52 def test_default_attributes
53 with_settings :default_projects_public => '1' do
53 with_settings :default_projects_public => '1' do
54 assert_equal true, Project.new.is_public
54 assert_equal true, Project.new.is_public
55 assert_equal false, Project.new(:is_public => false).is_public
55 assert_equal false, Project.new(:is_public => false).is_public
56 end
56 end
57
57
58 with_settings :default_projects_public => '0' do
58 with_settings :default_projects_public => '0' do
59 assert_equal false, Project.new.is_public
59 assert_equal false, Project.new.is_public
60 assert_equal true, Project.new(:is_public => true).is_public
60 assert_equal true, Project.new(:is_public => true).is_public
61 end
61 end
62
62
63 with_settings :sequential_project_identifiers => '1' do
63 with_settings :sequential_project_identifiers => '1' do
64 assert !Project.new.identifier.blank?
64 assert !Project.new.identifier.blank?
65 assert Project.new(:identifier => '').identifier.blank?
65 assert Project.new(:identifier => '').identifier.blank?
66 end
66 end
67
67
68 with_settings :sequential_project_identifiers => '0' do
68 with_settings :sequential_project_identifiers => '0' do
69 assert Project.new.identifier.blank?
69 assert Project.new.identifier.blank?
70 assert !Project.new(:identifier => 'test').blank?
70 assert !Project.new(:identifier => 'test').blank?
71 end
71 end
72
72
73 with_settings :default_projects_modules => ['issue_tracking', 'repository'] do
73 with_settings :default_projects_modules => ['issue_tracking', 'repository'] do
74 assert_equal ['issue_tracking', 'repository'], Project.new.enabled_module_names
74 assert_equal ['issue_tracking', 'repository'], Project.new.enabled_module_names
75 end
75 end
76
76
77 assert_equal Tracker.all.sort, Project.new.trackers.sort
77 assert_equal Tracker.all.sort, Project.new.trackers.sort
78 assert_equal Tracker.find(1, 3).sort, Project.new(:tracker_ids => [1, 3]).trackers.sort
78 assert_equal Tracker.find(1, 3).sort, Project.new(:tracker_ids => [1, 3]).trackers.sort
79 end
79 end
80
80
81 def test_update
81 def test_update
82 assert_equal "eCookbook", @ecookbook.name
82 assert_equal "eCookbook", @ecookbook.name
83 @ecookbook.name = "eCook"
83 @ecookbook.name = "eCook"
84 assert @ecookbook.save, @ecookbook.errors.full_messages.join("; ")
84 assert @ecookbook.save, @ecookbook.errors.full_messages.join("; ")
85 @ecookbook.reload
85 @ecookbook.reload
86 assert_equal "eCook", @ecookbook.name
86 assert_equal "eCook", @ecookbook.name
87 end
87 end
88
88
89 def test_validate_identifier
89 def test_validate_identifier
90 to_test = {"abc" => true,
90 to_test = {"abc" => true,
91 "ab12" => true,
91 "ab12" => true,
92 "ab-12" => true,
92 "ab-12" => true,
93 "ab_12" => true,
93 "ab_12" => true,
94 "12" => false,
94 "12" => false,
95 "new" => false}
95 "new" => false}
96
96
97 to_test.each do |identifier, valid|
97 to_test.each do |identifier, valid|
98 p = Project.new
98 p = Project.new
99 p.identifier = identifier
99 p.identifier = identifier
100 p.valid?
100 p.valid?
101 if valid
101 if valid
102 assert p.errors['identifier'].blank?, "identifier #{identifier} was not valid"
102 assert p.errors['identifier'].blank?, "identifier #{identifier} was not valid"
103 else
103 else
104 assert p.errors['identifier'].present?, "identifier #{identifier} was valid"
104 assert p.errors['identifier'].present?, "identifier #{identifier} was valid"
105 end
105 end
106 end
106 end
107 end
107 end
108
108
109 def test_members_should_be_active_users
109 def test_members_should_be_active_users
110 Project.all.each do |project|
110 Project.all.each do |project|
111 assert_nil project.members.detect {|m| !(m.user.is_a?(User) && m.user.active?) }
111 assert_nil project.members.detect {|m| !(m.user.is_a?(User) && m.user.active?) }
112 end
112 end
113 end
113 end
114
114
115 def test_users_should_be_active_users
115 def test_users_should_be_active_users
116 Project.all.each do |project|
116 Project.all.each do |project|
117 assert_nil project.users.detect {|u| !(u.is_a?(User) && u.active?) }
117 assert_nil project.users.detect {|u| !(u.is_a?(User) && u.active?) }
118 end
118 end
119 end
119 end
120
120
121 def test_archive
121 def test_archive
122 user = @ecookbook.members.first.user
122 user = @ecookbook.members.first.user
123 @ecookbook.archive
123 @ecookbook.archive
124 @ecookbook.reload
124 @ecookbook.reload
125
125
126 assert !@ecookbook.active?
126 assert !@ecookbook.active?
127 assert @ecookbook.archived?
127 assert @ecookbook.archived?
128 assert !user.projects.include?(@ecookbook)
128 assert !user.projects.include?(@ecookbook)
129 # Subproject are also archived
129 # Subproject are also archived
130 assert !@ecookbook.children.empty?
130 assert !@ecookbook.children.empty?
131 assert @ecookbook.descendants.active.empty?
131 assert @ecookbook.descendants.active.empty?
132 end
132 end
133
133
134 def test_archive_should_fail_if_versions_are_used_by_non_descendant_projects
134 def test_archive_should_fail_if_versions_are_used_by_non_descendant_projects
135 # Assign an issue of a project to a version of a child project
135 # Assign an issue of a project to a version of a child project
136 Issue.find(4).update_attribute :fixed_version_id, 4
136 Issue.find(4).update_attribute :fixed_version_id, 4
137
137
138 assert_no_difference "Project.count(:all, :conditions => 'status = #{Project::STATUS_ARCHIVED}')" do
138 assert_no_difference "Project.count(:all, :conditions => 'status = #{Project::STATUS_ARCHIVED}')" do
139 assert_equal false, @ecookbook.archive
139 assert_equal false, @ecookbook.archive
140 end
140 end
141 @ecookbook.reload
141 @ecookbook.reload
142 assert @ecookbook.active?
142 assert @ecookbook.active?
143 end
143 end
144
144
145 def test_unarchive
145 def test_unarchive
146 user = @ecookbook.members.first.user
146 user = @ecookbook.members.first.user
147 @ecookbook.archive
147 @ecookbook.archive
148 # A subproject of an archived project can not be unarchived
148 # A subproject of an archived project can not be unarchived
149 assert !@ecookbook_sub1.unarchive
149 assert !@ecookbook_sub1.unarchive
150
150
151 # Unarchive project
151 # Unarchive project
152 assert @ecookbook.unarchive
152 assert @ecookbook.unarchive
153 @ecookbook.reload
153 @ecookbook.reload
154 assert @ecookbook.active?
154 assert @ecookbook.active?
155 assert !@ecookbook.archived?
155 assert !@ecookbook.archived?
156 assert user.projects.include?(@ecookbook)
156 assert user.projects.include?(@ecookbook)
157 # Subproject can now be unarchived
157 # Subproject can now be unarchived
158 @ecookbook_sub1.reload
158 @ecookbook_sub1.reload
159 assert @ecookbook_sub1.unarchive
159 assert @ecookbook_sub1.unarchive
160 end
160 end
161
161
162 def test_destroy
162 def test_destroy
163 # 2 active members
163 # 2 active members
164 assert_equal 2, @ecookbook.members.size
164 assert_equal 2, @ecookbook.members.size
165 # and 1 is locked
165 # and 1 is locked
166 assert_equal 3, Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).size
166 assert_equal 3, Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).size
167 # some boards
167 # some boards
168 assert @ecookbook.boards.any?
168 assert @ecookbook.boards.any?
169
169
170 @ecookbook.destroy
170 @ecookbook.destroy
171 # make sure that the project non longer exists
171 # make sure that the project non longer exists
172 assert_raise(ActiveRecord::RecordNotFound) { Project.find(@ecookbook.id) }
172 assert_raise(ActiveRecord::RecordNotFound) { Project.find(@ecookbook.id) }
173 # make sure related data was removed
173 # make sure related data was removed
174 assert_nil Member.first(:conditions => {:project_id => @ecookbook.id})
174 assert_nil Member.first(:conditions => {:project_id => @ecookbook.id})
175 assert_nil Board.first(:conditions => {:project_id => @ecookbook.id})
175 assert_nil Board.first(:conditions => {:project_id => @ecookbook.id})
176 assert_nil Issue.first(:conditions => {:project_id => @ecookbook.id})
176 assert_nil Issue.first(:conditions => {:project_id => @ecookbook.id})
177 end
177 end
178
178
179 def test_destroy_should_destroy_subtasks
180 issues = (0..2).to_a.map {Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1, :subject => 'test')}
181 issues[0].update_attribute :parent_issue_id, issues[1].id
182 issues[2].update_attribute :parent_issue_id, issues[1].id
183 assert_equal 2, issues[1].children.count
184
185 assert_nothing_raised do
186 Project.find(1).destroy
187 end
188 assert Issue.find_all_by_id(issues.map(&:id)).empty?
189 end
190
179 def test_destroying_root_projects_should_clear_data
191 def test_destroying_root_projects_should_clear_data
180 Project.roots.each do |root|
192 Project.roots.each do |root|
181 root.destroy
193 root.destroy
182 end
194 end
183
195
184 assert_equal 0, Project.count, "Projects were not deleted: #{Project.all.inspect}"
196 assert_equal 0, Project.count, "Projects were not deleted: #{Project.all.inspect}"
185 assert_equal 0, Member.count, "Members were not deleted: #{Member.all.inspect}"
197 assert_equal 0, Member.count, "Members were not deleted: #{Member.all.inspect}"
186 assert_equal 0, MemberRole.count
198 assert_equal 0, MemberRole.count
187 assert_equal 0, Issue.count
199 assert_equal 0, Issue.count
188 assert_equal 0, Journal.count
200 assert_equal 0, Journal.count
189 assert_equal 0, JournalDetail.count
201 assert_equal 0, JournalDetail.count
190 assert_equal 0, Attachment.count
202 assert_equal 0, Attachment.count
191 assert_equal 0, EnabledModule.count
203 assert_equal 0, EnabledModule.count
192 assert_equal 0, IssueCategory.count
204 assert_equal 0, IssueCategory.count
193 assert_equal 0, IssueRelation.count
205 assert_equal 0, IssueRelation.count
194 assert_equal 0, Board.count
206 assert_equal 0, Board.count
195 assert_equal 0, Message.count
207 assert_equal 0, Message.count
196 assert_equal 0, News.count
208 assert_equal 0, News.count
197 assert_equal 0, Query.count(:conditions => "project_id IS NOT NULL")
209 assert_equal 0, Query.count(:conditions => "project_id IS NOT NULL")
198 assert_equal 0, Repository.count
210 assert_equal 0, Repository.count
199 assert_equal 0, Changeset.count
211 assert_equal 0, Changeset.count
200 assert_equal 0, Change.count
212 assert_equal 0, Change.count
201 assert_equal 0, Comment.count
213 assert_equal 0, Comment.count
202 assert_equal 0, TimeEntry.count
214 assert_equal 0, TimeEntry.count
203 assert_equal 0, Version.count
215 assert_equal 0, Version.count
204 assert_equal 0, Watcher.count
216 assert_equal 0, Watcher.count
205 assert_equal 0, Wiki.count
217 assert_equal 0, Wiki.count
206 assert_equal 0, WikiPage.count
218 assert_equal 0, WikiPage.count
207 assert_equal 0, WikiContent.count
219 assert_equal 0, WikiContent.count
208 assert_equal 0, WikiContent::Version.count
220 assert_equal 0, WikiContent::Version.count
209 assert_equal 0, Project.connection.select_all("SELECT * FROM projects_trackers").size
221 assert_equal 0, Project.connection.select_all("SELECT * FROM projects_trackers").size
210 assert_equal 0, Project.connection.select_all("SELECT * FROM custom_fields_projects").size
222 assert_equal 0, Project.connection.select_all("SELECT * FROM custom_fields_projects").size
211 assert_equal 0, CustomValue.count(:conditions => {:customized_type => ['Project', 'Issue', 'TimeEntry', 'Version']})
223 assert_equal 0, CustomValue.count(:conditions => {:customized_type => ['Project', 'Issue', 'TimeEntry', 'Version']})
212 end
224 end
213
225
214 def test_move_an_orphan_project_to_a_root_project
226 def test_move_an_orphan_project_to_a_root_project
215 sub = Project.find(2)
227 sub = Project.find(2)
216 sub.set_parent! @ecookbook
228 sub.set_parent! @ecookbook
217 assert_equal @ecookbook.id, sub.parent.id
229 assert_equal @ecookbook.id, sub.parent.id
218 @ecookbook.reload
230 @ecookbook.reload
219 assert_equal 4, @ecookbook.children.size
231 assert_equal 4, @ecookbook.children.size
220 end
232 end
221
233
222 def test_move_an_orphan_project_to_a_subproject
234 def test_move_an_orphan_project_to_a_subproject
223 sub = Project.find(2)
235 sub = Project.find(2)
224 assert sub.set_parent!(@ecookbook_sub1)
236 assert sub.set_parent!(@ecookbook_sub1)
225 end
237 end
226
238
227 def test_move_a_root_project_to_a_project
239 def test_move_a_root_project_to_a_project
228 sub = @ecookbook
240 sub = @ecookbook
229 assert sub.set_parent!(Project.find(2))
241 assert sub.set_parent!(Project.find(2))
230 end
242 end
231
243
232 def test_should_not_move_a_project_to_its_children
244 def test_should_not_move_a_project_to_its_children
233 sub = @ecookbook
245 sub = @ecookbook
234 assert !(sub.set_parent!(Project.find(3)))
246 assert !(sub.set_parent!(Project.find(3)))
235 end
247 end
236
248
237 def test_set_parent_should_add_roots_in_alphabetical_order
249 def test_set_parent_should_add_roots_in_alphabetical_order
238 ProjectCustomField.delete_all
250 ProjectCustomField.delete_all
239 Project.delete_all
251 Project.delete_all
240 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(nil)
252 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(nil)
241 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(nil)
253 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(nil)
242 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(nil)
254 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(nil)
243 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(nil)
255 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(nil)
244
256
245 assert_equal 4, Project.count
257 assert_equal 4, Project.count
246 assert_equal Project.all.sort_by(&:name), Project.all.sort_by(&:lft)
258 assert_equal Project.all.sort_by(&:name), Project.all.sort_by(&:lft)
247 end
259 end
248
260
249 def test_set_parent_should_add_children_in_alphabetical_order
261 def test_set_parent_should_add_children_in_alphabetical_order
250 ProjectCustomField.delete_all
262 ProjectCustomField.delete_all
251 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
263 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
252 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(parent)
264 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(parent)
253 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(parent)
265 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(parent)
254 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(parent)
266 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(parent)
255 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(parent)
267 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(parent)
256
268
257 parent.reload
269 parent.reload
258 assert_equal 4, parent.children.size
270 assert_equal 4, parent.children.size
259 assert_equal parent.children.all.sort_by(&:name), parent.children.all
271 assert_equal parent.children.all.sort_by(&:name), parent.children.all
260 end
272 end
261
273
262 def test_rebuild_should_sort_children_alphabetically
274 def test_rebuild_should_sort_children_alphabetically
263 ProjectCustomField.delete_all
275 ProjectCustomField.delete_all
264 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
276 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
265 Project.create!(:name => 'Project C', :identifier => 'project-c').move_to_child_of(parent)
277 Project.create!(:name => 'Project C', :identifier => 'project-c').move_to_child_of(parent)
266 Project.create!(:name => 'Project B', :identifier => 'project-b').move_to_child_of(parent)
278 Project.create!(:name => 'Project B', :identifier => 'project-b').move_to_child_of(parent)
267 Project.create!(:name => 'Project D', :identifier => 'project-d').move_to_child_of(parent)
279 Project.create!(:name => 'Project D', :identifier => 'project-d').move_to_child_of(parent)
268 Project.create!(:name => 'Project A', :identifier => 'project-a').move_to_child_of(parent)
280 Project.create!(:name => 'Project A', :identifier => 'project-a').move_to_child_of(parent)
269
281
270 Project.update_all("lft = NULL, rgt = NULL")
282 Project.update_all("lft = NULL, rgt = NULL")
271 Project.rebuild!
283 Project.rebuild!
272
284
273 parent.reload
285 parent.reload
274 assert_equal 4, parent.children.size
286 assert_equal 4, parent.children.size
275 assert_equal parent.children.all.sort_by(&:name), parent.children.all
287 assert_equal parent.children.all.sort_by(&:name), parent.children.all
276 end
288 end
277
289
278
290
279 def test_set_parent_should_update_issue_fixed_version_associations_when_a_fixed_version_is_moved_out_of_the_hierarchy
291 def test_set_parent_should_update_issue_fixed_version_associations_when_a_fixed_version_is_moved_out_of_the_hierarchy
280 # Parent issue with a hierarchy project's fixed version
292 # Parent issue with a hierarchy project's fixed version
281 parent_issue = Issue.find(1)
293 parent_issue = Issue.find(1)
282 parent_issue.update_attribute(:fixed_version_id, 4)
294 parent_issue.update_attribute(:fixed_version_id, 4)
283 parent_issue.reload
295 parent_issue.reload
284 assert_equal 4, parent_issue.fixed_version_id
296 assert_equal 4, parent_issue.fixed_version_id
285
297
286 # Should keep fixed versions for the issues
298 # Should keep fixed versions for the issues
287 issue_with_local_fixed_version = Issue.find(5)
299 issue_with_local_fixed_version = Issue.find(5)
288 issue_with_local_fixed_version.update_attribute(:fixed_version_id, 4)
300 issue_with_local_fixed_version.update_attribute(:fixed_version_id, 4)
289 issue_with_local_fixed_version.reload
301 issue_with_local_fixed_version.reload
290 assert_equal 4, issue_with_local_fixed_version.fixed_version_id
302 assert_equal 4, issue_with_local_fixed_version.fixed_version_id
291
303
292 # Local issue with hierarchy fixed_version
304 # Local issue with hierarchy fixed_version
293 issue_with_hierarchy_fixed_version = Issue.find(13)
305 issue_with_hierarchy_fixed_version = Issue.find(13)
294 issue_with_hierarchy_fixed_version.update_attribute(:fixed_version_id, 6)
306 issue_with_hierarchy_fixed_version.update_attribute(:fixed_version_id, 6)
295 issue_with_hierarchy_fixed_version.reload
307 issue_with_hierarchy_fixed_version.reload
296 assert_equal 6, issue_with_hierarchy_fixed_version.fixed_version_id
308 assert_equal 6, issue_with_hierarchy_fixed_version.fixed_version_id
297
309
298 # Move project out of the issue's hierarchy
310 # Move project out of the issue's hierarchy
299 moved_project = Project.find(3)
311 moved_project = Project.find(3)
300 moved_project.set_parent!(Project.find(2))
312 moved_project.set_parent!(Project.find(2))
301 parent_issue.reload
313 parent_issue.reload
302 issue_with_local_fixed_version.reload
314 issue_with_local_fixed_version.reload
303 issue_with_hierarchy_fixed_version.reload
315 issue_with_hierarchy_fixed_version.reload
304
316
305 assert_equal 4, issue_with_local_fixed_version.fixed_version_id, "Fixed version was not keep on an issue local to the moved project"
317 assert_equal 4, issue_with_local_fixed_version.fixed_version_id, "Fixed version was not keep on an issue local to the moved project"
306 assert_equal nil, issue_with_hierarchy_fixed_version.fixed_version_id, "Fixed version is still set after moving the Project out of the hierarchy where the version is defined in"
318 assert_equal nil, issue_with_hierarchy_fixed_version.fixed_version_id, "Fixed version is still set after moving the Project out of the hierarchy where the version is defined in"
307 assert_equal nil, parent_issue.fixed_version_id, "Fixed version is still set after moving the Version out of the hierarchy for the issue."
319 assert_equal nil, parent_issue.fixed_version_id, "Fixed version is still set after moving the Version out of the hierarchy for the issue."
308 end
320 end
309
321
310 def test_parent
322 def test_parent
311 p = Project.find(6).parent
323 p = Project.find(6).parent
312 assert p.is_a?(Project)
324 assert p.is_a?(Project)
313 assert_equal 5, p.id
325 assert_equal 5, p.id
314 end
326 end
315
327
316 def test_ancestors
328 def test_ancestors
317 a = Project.find(6).ancestors
329 a = Project.find(6).ancestors
318 assert a.first.is_a?(Project)
330 assert a.first.is_a?(Project)
319 assert_equal [1, 5], a.collect(&:id)
331 assert_equal [1, 5], a.collect(&:id)
320 end
332 end
321
333
322 def test_root
334 def test_root
323 r = Project.find(6).root
335 r = Project.find(6).root
324 assert r.is_a?(Project)
336 assert r.is_a?(Project)
325 assert_equal 1, r.id
337 assert_equal 1, r.id
326 end
338 end
327
339
328 def test_children
340 def test_children
329 c = Project.find(1).children
341 c = Project.find(1).children
330 assert c.first.is_a?(Project)
342 assert c.first.is_a?(Project)
331 assert_equal [5, 3, 4], c.collect(&:id)
343 assert_equal [5, 3, 4], c.collect(&:id)
332 end
344 end
333
345
334 def test_descendants
346 def test_descendants
335 d = Project.find(1).descendants
347 d = Project.find(1).descendants
336 assert d.first.is_a?(Project)
348 assert d.first.is_a?(Project)
337 assert_equal [5, 6, 3, 4], d.collect(&:id)
349 assert_equal [5, 6, 3, 4], d.collect(&:id)
338 end
350 end
339
351
340 def test_allowed_parents_should_be_empty_for_non_member_user
352 def test_allowed_parents_should_be_empty_for_non_member_user
341 Role.non_member.add_permission!(:add_project)
353 Role.non_member.add_permission!(:add_project)
342 user = User.find(9)
354 user = User.find(9)
343 assert user.memberships.empty?
355 assert user.memberships.empty?
344 User.current = user
356 User.current = user
345 assert Project.new.allowed_parents.compact.empty?
357 assert Project.new.allowed_parents.compact.empty?
346 end
358 end
347
359
348 def test_allowed_parents_with_add_subprojects_permission
360 def test_allowed_parents_with_add_subprojects_permission
349 Role.find(1).remove_permission!(:add_project)
361 Role.find(1).remove_permission!(:add_project)
350 Role.find(1).add_permission!(:add_subprojects)
362 Role.find(1).add_permission!(:add_subprojects)
351 User.current = User.find(2)
363 User.current = User.find(2)
352 # new project
364 # new project
353 assert !Project.new.allowed_parents.include?(nil)
365 assert !Project.new.allowed_parents.include?(nil)
354 assert Project.new.allowed_parents.include?(Project.find(1))
366 assert Project.new.allowed_parents.include?(Project.find(1))
355 # existing root project
367 # existing root project
356 assert Project.find(1).allowed_parents.include?(nil)
368 assert Project.find(1).allowed_parents.include?(nil)
357 # existing child
369 # existing child
358 assert Project.find(3).allowed_parents.include?(Project.find(1))
370 assert Project.find(3).allowed_parents.include?(Project.find(1))
359 assert !Project.find(3).allowed_parents.include?(nil)
371 assert !Project.find(3).allowed_parents.include?(nil)
360 end
372 end
361
373
362 def test_allowed_parents_with_add_project_permission
374 def test_allowed_parents_with_add_project_permission
363 Role.find(1).add_permission!(:add_project)
375 Role.find(1).add_permission!(:add_project)
364 Role.find(1).remove_permission!(:add_subprojects)
376 Role.find(1).remove_permission!(:add_subprojects)
365 User.current = User.find(2)
377 User.current = User.find(2)
366 # new project
378 # new project
367 assert Project.new.allowed_parents.include?(nil)
379 assert Project.new.allowed_parents.include?(nil)
368 assert !Project.new.allowed_parents.include?(Project.find(1))
380 assert !Project.new.allowed_parents.include?(Project.find(1))
369 # existing root project
381 # existing root project
370 assert Project.find(1).allowed_parents.include?(nil)
382 assert Project.find(1).allowed_parents.include?(nil)
371 # existing child
383 # existing child
372 assert Project.find(3).allowed_parents.include?(Project.find(1))
384 assert Project.find(3).allowed_parents.include?(Project.find(1))
373 assert Project.find(3).allowed_parents.include?(nil)
385 assert Project.find(3).allowed_parents.include?(nil)
374 end
386 end
375
387
376 def test_allowed_parents_with_add_project_and_subprojects_permission
388 def test_allowed_parents_with_add_project_and_subprojects_permission
377 Role.find(1).add_permission!(:add_project)
389 Role.find(1).add_permission!(:add_project)
378 Role.find(1).add_permission!(:add_subprojects)
390 Role.find(1).add_permission!(:add_subprojects)
379 User.current = User.find(2)
391 User.current = User.find(2)
380 # new project
392 # new project
381 assert Project.new.allowed_parents.include?(nil)
393 assert Project.new.allowed_parents.include?(nil)
382 assert Project.new.allowed_parents.include?(Project.find(1))
394 assert Project.new.allowed_parents.include?(Project.find(1))
383 # existing root project
395 # existing root project
384 assert Project.find(1).allowed_parents.include?(nil)
396 assert Project.find(1).allowed_parents.include?(nil)
385 # existing child
397 # existing child
386 assert Project.find(3).allowed_parents.include?(Project.find(1))
398 assert Project.find(3).allowed_parents.include?(Project.find(1))
387 assert Project.find(3).allowed_parents.include?(nil)
399 assert Project.find(3).allowed_parents.include?(nil)
388 end
400 end
389
401
390 def test_users_by_role
402 def test_users_by_role
391 users_by_role = Project.find(1).users_by_role
403 users_by_role = Project.find(1).users_by_role
392 assert_kind_of Hash, users_by_role
404 assert_kind_of Hash, users_by_role
393 role = Role.find(1)
405 role = Role.find(1)
394 assert_kind_of Array, users_by_role[role]
406 assert_kind_of Array, users_by_role[role]
395 assert users_by_role[role].include?(User.find(2))
407 assert users_by_role[role].include?(User.find(2))
396 end
408 end
397
409
398 def test_rolled_up_trackers
410 def test_rolled_up_trackers
399 parent = Project.find(1)
411 parent = Project.find(1)
400 parent.trackers = Tracker.find([1,2])
412 parent.trackers = Tracker.find([1,2])
401 child = parent.children.find(3)
413 child = parent.children.find(3)
402
414
403 assert_equal [1, 2], parent.tracker_ids
415 assert_equal [1, 2], parent.tracker_ids
404 assert_equal [2, 3], child.trackers.collect(&:id)
416 assert_equal [2, 3], child.trackers.collect(&:id)
405
417
406 assert_kind_of Tracker, parent.rolled_up_trackers.first
418 assert_kind_of Tracker, parent.rolled_up_trackers.first
407 assert_equal Tracker.find(1), parent.rolled_up_trackers.first
419 assert_equal Tracker.find(1), parent.rolled_up_trackers.first
408
420
409 assert_equal [1, 2, 3], parent.rolled_up_trackers.collect(&:id)
421 assert_equal [1, 2, 3], parent.rolled_up_trackers.collect(&:id)
410 assert_equal [2, 3], child.rolled_up_trackers.collect(&:id)
422 assert_equal [2, 3], child.rolled_up_trackers.collect(&:id)
411 end
423 end
412
424
413 def test_rolled_up_trackers_should_ignore_archived_subprojects
425 def test_rolled_up_trackers_should_ignore_archived_subprojects
414 parent = Project.find(1)
426 parent = Project.find(1)
415 parent.trackers = Tracker.find([1,2])
427 parent.trackers = Tracker.find([1,2])
416 child = parent.children.find(3)
428 child = parent.children.find(3)
417 child.trackers = Tracker.find([1,3])
429 child.trackers = Tracker.find([1,3])
418 parent.children.each(&:archive)
430 parent.children.each(&:archive)
419
431
420 assert_equal [1,2], parent.rolled_up_trackers.collect(&:id)
432 assert_equal [1,2], parent.rolled_up_trackers.collect(&:id)
421 end
433 end
422
434
423 context "#rolled_up_versions" do
435 context "#rolled_up_versions" do
424 setup do
436 setup do
425 @project = Project.generate!
437 @project = Project.generate!
426 @parent_version_1 = Version.generate!(:project => @project)
438 @parent_version_1 = Version.generate!(:project => @project)
427 @parent_version_2 = Version.generate!(:project => @project)
439 @parent_version_2 = Version.generate!(:project => @project)
428 end
440 end
429
441
430 should "include the versions for the current project" do
442 should "include the versions for the current project" do
431 assert_same_elements [@parent_version_1, @parent_version_2], @project.rolled_up_versions
443 assert_same_elements [@parent_version_1, @parent_version_2], @project.rolled_up_versions
432 end
444 end
433
445
434 should "include versions for a subproject" do
446 should "include versions for a subproject" do
435 @subproject = Project.generate!
447 @subproject = Project.generate!
436 @subproject.set_parent!(@project)
448 @subproject.set_parent!(@project)
437 @subproject_version = Version.generate!(:project => @subproject)
449 @subproject_version = Version.generate!(:project => @subproject)
438
450
439 assert_same_elements [
451 assert_same_elements [
440 @parent_version_1,
452 @parent_version_1,
441 @parent_version_2,
453 @parent_version_2,
442 @subproject_version
454 @subproject_version
443 ], @project.rolled_up_versions
455 ], @project.rolled_up_versions
444 end
456 end
445
457
446 should "include versions for a sub-subproject" do
458 should "include versions for a sub-subproject" do
447 @subproject = Project.generate!
459 @subproject = Project.generate!
448 @subproject.set_parent!(@project)
460 @subproject.set_parent!(@project)
449 @sub_subproject = Project.generate!
461 @sub_subproject = Project.generate!
450 @sub_subproject.set_parent!(@subproject)
462 @sub_subproject.set_parent!(@subproject)
451 @sub_subproject_version = Version.generate!(:project => @sub_subproject)
463 @sub_subproject_version = Version.generate!(:project => @sub_subproject)
452
464
453 @project.reload
465 @project.reload
454
466
455 assert_same_elements [
467 assert_same_elements [
456 @parent_version_1,
468 @parent_version_1,
457 @parent_version_2,
469 @parent_version_2,
458 @sub_subproject_version
470 @sub_subproject_version
459 ], @project.rolled_up_versions
471 ], @project.rolled_up_versions
460 end
472 end
461
473
462 should "only check active projects" do
474 should "only check active projects" do
463 @subproject = Project.generate!
475 @subproject = Project.generate!
464 @subproject.set_parent!(@project)
476 @subproject.set_parent!(@project)
465 @subproject_version = Version.generate!(:project => @subproject)
477 @subproject_version = Version.generate!(:project => @subproject)
466 assert @subproject.archive
478 assert @subproject.archive
467
479
468 @project.reload
480 @project.reload
469
481
470 assert !@subproject.active?
482 assert !@subproject.active?
471 assert_same_elements [@parent_version_1, @parent_version_2], @project.rolled_up_versions
483 assert_same_elements [@parent_version_1, @parent_version_2], @project.rolled_up_versions
472 end
484 end
473 end
485 end
474
486
475 def test_shared_versions_none_sharing
487 def test_shared_versions_none_sharing
476 p = Project.find(5)
488 p = Project.find(5)
477 v = Version.create!(:name => 'none_sharing', :project => p, :sharing => 'none')
489 v = Version.create!(:name => 'none_sharing', :project => p, :sharing => 'none')
478 assert p.shared_versions.include?(v)
490 assert p.shared_versions.include?(v)
479 assert !p.children.first.shared_versions.include?(v)
491 assert !p.children.first.shared_versions.include?(v)
480 assert !p.root.shared_versions.include?(v)
492 assert !p.root.shared_versions.include?(v)
481 assert !p.siblings.first.shared_versions.include?(v)
493 assert !p.siblings.first.shared_versions.include?(v)
482 assert !p.root.siblings.first.shared_versions.include?(v)
494 assert !p.root.siblings.first.shared_versions.include?(v)
483 end
495 end
484
496
485 def test_shared_versions_descendants_sharing
497 def test_shared_versions_descendants_sharing
486 p = Project.find(5)
498 p = Project.find(5)
487 v = Version.create!(:name => 'descendants_sharing', :project => p, :sharing => 'descendants')
499 v = Version.create!(:name => 'descendants_sharing', :project => p, :sharing => 'descendants')
488 assert p.shared_versions.include?(v)
500 assert p.shared_versions.include?(v)
489 assert p.children.first.shared_versions.include?(v)
501 assert p.children.first.shared_versions.include?(v)
490 assert !p.root.shared_versions.include?(v)
502 assert !p.root.shared_versions.include?(v)
491 assert !p.siblings.first.shared_versions.include?(v)
503 assert !p.siblings.first.shared_versions.include?(v)
492 assert !p.root.siblings.first.shared_versions.include?(v)
504 assert !p.root.siblings.first.shared_versions.include?(v)
493 end
505 end
494
506
495 def test_shared_versions_hierarchy_sharing
507 def test_shared_versions_hierarchy_sharing
496 p = Project.find(5)
508 p = Project.find(5)
497 v = Version.create!(:name => 'hierarchy_sharing', :project => p, :sharing => 'hierarchy')
509 v = Version.create!(:name => 'hierarchy_sharing', :project => p, :sharing => 'hierarchy')
498 assert p.shared_versions.include?(v)
510 assert p.shared_versions.include?(v)
499 assert p.children.first.shared_versions.include?(v)
511 assert p.children.first.shared_versions.include?(v)
500 assert p.root.shared_versions.include?(v)
512 assert p.root.shared_versions.include?(v)
501 assert !p.siblings.first.shared_versions.include?(v)
513 assert !p.siblings.first.shared_versions.include?(v)
502 assert !p.root.siblings.first.shared_versions.include?(v)
514 assert !p.root.siblings.first.shared_versions.include?(v)
503 end
515 end
504
516
505 def test_shared_versions_tree_sharing
517 def test_shared_versions_tree_sharing
506 p = Project.find(5)
518 p = Project.find(5)
507 v = Version.create!(:name => 'tree_sharing', :project => p, :sharing => 'tree')
519 v = Version.create!(:name => 'tree_sharing', :project => p, :sharing => 'tree')
508 assert p.shared_versions.include?(v)
520 assert p.shared_versions.include?(v)
509 assert p.children.first.shared_versions.include?(v)
521 assert p.children.first.shared_versions.include?(v)
510 assert p.root.shared_versions.include?(v)
522 assert p.root.shared_versions.include?(v)
511 assert p.siblings.first.shared_versions.include?(v)
523 assert p.siblings.first.shared_versions.include?(v)
512 assert !p.root.siblings.first.shared_versions.include?(v)
524 assert !p.root.siblings.first.shared_versions.include?(v)
513 end
525 end
514
526
515 def test_shared_versions_system_sharing
527 def test_shared_versions_system_sharing
516 p = Project.find(5)
528 p = Project.find(5)
517 v = Version.create!(:name => 'system_sharing', :project => p, :sharing => 'system')
529 v = Version.create!(:name => 'system_sharing', :project => p, :sharing => 'system')
518 assert p.shared_versions.include?(v)
530 assert p.shared_versions.include?(v)
519 assert p.children.first.shared_versions.include?(v)
531 assert p.children.first.shared_versions.include?(v)
520 assert p.root.shared_versions.include?(v)
532 assert p.root.shared_versions.include?(v)
521 assert p.siblings.first.shared_versions.include?(v)
533 assert p.siblings.first.shared_versions.include?(v)
522 assert p.root.siblings.first.shared_versions.include?(v)
534 assert p.root.siblings.first.shared_versions.include?(v)
523 end
535 end
524
536
525 def test_shared_versions
537 def test_shared_versions
526 parent = Project.find(1)
538 parent = Project.find(1)
527 child = parent.children.find(3)
539 child = parent.children.find(3)
528 private_child = parent.children.find(5)
540 private_child = parent.children.find(5)
529
541
530 assert_equal [1,2,3], parent.version_ids.sort
542 assert_equal [1,2,3], parent.version_ids.sort
531 assert_equal [4], child.version_ids
543 assert_equal [4], child.version_ids
532 assert_equal [6], private_child.version_ids
544 assert_equal [6], private_child.version_ids
533 assert_equal [7], Version.find_all_by_sharing('system').collect(&:id)
545 assert_equal [7], Version.find_all_by_sharing('system').collect(&:id)
534
546
535 assert_equal 6, parent.shared_versions.size
547 assert_equal 6, parent.shared_versions.size
536 parent.shared_versions.each do |version|
548 parent.shared_versions.each do |version|
537 assert_kind_of Version, version
549 assert_kind_of Version, version
538 end
550 end
539
551
540 assert_equal [1,2,3,4,6,7], parent.shared_versions.collect(&:id).sort
552 assert_equal [1,2,3,4,6,7], parent.shared_versions.collect(&:id).sort
541 end
553 end
542
554
543 def test_shared_versions_should_ignore_archived_subprojects
555 def test_shared_versions_should_ignore_archived_subprojects
544 parent = Project.find(1)
556 parent = Project.find(1)
545 child = parent.children.find(3)
557 child = parent.children.find(3)
546 child.archive
558 child.archive
547 parent.reload
559 parent.reload
548
560
549 assert_equal [1,2,3], parent.version_ids.sort
561 assert_equal [1,2,3], parent.version_ids.sort
550 assert_equal [4], child.version_ids
562 assert_equal [4], child.version_ids
551 assert !parent.shared_versions.collect(&:id).include?(4)
563 assert !parent.shared_versions.collect(&:id).include?(4)
552 end
564 end
553
565
554 def test_shared_versions_visible_to_user
566 def test_shared_versions_visible_to_user
555 user = User.find(3)
567 user = User.find(3)
556 parent = Project.find(1)
568 parent = Project.find(1)
557 child = parent.children.find(5)
569 child = parent.children.find(5)
558
570
559 assert_equal [1,2,3], parent.version_ids.sort
571 assert_equal [1,2,3], parent.version_ids.sort
560 assert_equal [6], child.version_ids
572 assert_equal [6], child.version_ids
561
573
562 versions = parent.shared_versions.visible(user)
574 versions = parent.shared_versions.visible(user)
563
575
564 assert_equal 4, versions.size
576 assert_equal 4, versions.size
565 versions.each do |version|
577 versions.each do |version|
566 assert_kind_of Version, version
578 assert_kind_of Version, version
567 end
579 end
568
580
569 assert !versions.collect(&:id).include?(6)
581 assert !versions.collect(&:id).include?(6)
570 end
582 end
571
583
572 def test_shared_versions_for_new_project_should_include_system_shared_versions
584 def test_shared_versions_for_new_project_should_include_system_shared_versions
573 p = Project.find(5)
585 p = Project.find(5)
574 v = Version.create!(:name => 'system_sharing', :project => p, :sharing => 'system')
586 v = Version.create!(:name => 'system_sharing', :project => p, :sharing => 'system')
575
587
576 assert_include v, Project.new.shared_versions
588 assert_include v, Project.new.shared_versions
577 end
589 end
578
590
579 def test_next_identifier
591 def test_next_identifier
580 ProjectCustomField.delete_all
592 ProjectCustomField.delete_all
581 Project.create!(:name => 'last', :identifier => 'p2008040')
593 Project.create!(:name => 'last', :identifier => 'p2008040')
582 assert_equal 'p2008041', Project.next_identifier
594 assert_equal 'p2008041', Project.next_identifier
583 end
595 end
584
596
585 def test_next_identifier_first_project
597 def test_next_identifier_first_project
586 Project.delete_all
598 Project.delete_all
587 assert_nil Project.next_identifier
599 assert_nil Project.next_identifier
588 end
600 end
589
601
590 def test_enabled_module_names
602 def test_enabled_module_names
591 with_settings :default_projects_modules => ['issue_tracking', 'repository'] do
603 with_settings :default_projects_modules => ['issue_tracking', 'repository'] do
592 project = Project.new
604 project = Project.new
593
605
594 project.enabled_module_names = %w(issue_tracking news)
606 project.enabled_module_names = %w(issue_tracking news)
595 assert_equal %w(issue_tracking news), project.enabled_module_names.sort
607 assert_equal %w(issue_tracking news), project.enabled_module_names.sort
596 end
608 end
597 end
609 end
598
610
599 context "enabled_modules" do
611 context "enabled_modules" do
600 setup do
612 setup do
601 @project = Project.find(1)
613 @project = Project.find(1)
602 end
614 end
603
615
604 should "define module by names and preserve ids" do
616 should "define module by names and preserve ids" do
605 # Remove one module
617 # Remove one module
606 modules = @project.enabled_modules.slice(0..-2)
618 modules = @project.enabled_modules.slice(0..-2)
607 assert modules.any?
619 assert modules.any?
608 assert_difference 'EnabledModule.count', -1 do
620 assert_difference 'EnabledModule.count', -1 do
609 @project.enabled_module_names = modules.collect(&:name)
621 @project.enabled_module_names = modules.collect(&:name)
610 end
622 end
611 @project.reload
623 @project.reload
612 # Ids should be preserved
624 # Ids should be preserved
613 assert_equal @project.enabled_module_ids.sort, modules.collect(&:id).sort
625 assert_equal @project.enabled_module_ids.sort, modules.collect(&:id).sort
614 end
626 end
615
627
616 should "enable a module" do
628 should "enable a module" do
617 @project.enabled_module_names = []
629 @project.enabled_module_names = []
618 @project.reload
630 @project.reload
619 assert_equal [], @project.enabled_module_names
631 assert_equal [], @project.enabled_module_names
620 #with string
632 #with string
621 @project.enable_module!("issue_tracking")
633 @project.enable_module!("issue_tracking")
622 assert_equal ["issue_tracking"], @project.enabled_module_names
634 assert_equal ["issue_tracking"], @project.enabled_module_names
623 #with symbol
635 #with symbol
624 @project.enable_module!(:gantt)
636 @project.enable_module!(:gantt)
625 assert_equal ["issue_tracking", "gantt"], @project.enabled_module_names
637 assert_equal ["issue_tracking", "gantt"], @project.enabled_module_names
626 #don't add a module twice
638 #don't add a module twice
627 @project.enable_module!("issue_tracking")
639 @project.enable_module!("issue_tracking")
628 assert_equal ["issue_tracking", "gantt"], @project.enabled_module_names
640 assert_equal ["issue_tracking", "gantt"], @project.enabled_module_names
629 end
641 end
630
642
631 should "disable a module" do
643 should "disable a module" do
632 #with string
644 #with string
633 assert @project.enabled_module_names.include?("issue_tracking")
645 assert @project.enabled_module_names.include?("issue_tracking")
634 @project.disable_module!("issue_tracking")
646 @project.disable_module!("issue_tracking")
635 assert ! @project.reload.enabled_module_names.include?("issue_tracking")
647 assert ! @project.reload.enabled_module_names.include?("issue_tracking")
636 #with symbol
648 #with symbol
637 assert @project.enabled_module_names.include?("gantt")
649 assert @project.enabled_module_names.include?("gantt")
638 @project.disable_module!(:gantt)
650 @project.disable_module!(:gantt)
639 assert ! @project.reload.enabled_module_names.include?("gantt")
651 assert ! @project.reload.enabled_module_names.include?("gantt")
640 #with EnabledModule object
652 #with EnabledModule object
641 first_module = @project.enabled_modules.first
653 first_module = @project.enabled_modules.first
642 @project.disable_module!(first_module)
654 @project.disable_module!(first_module)
643 assert ! @project.reload.enabled_module_names.include?(first_module.name)
655 assert ! @project.reload.enabled_module_names.include?(first_module.name)
644 end
656 end
645 end
657 end
646
658
647 def test_enabled_module_names_should_not_recreate_enabled_modules
659 def test_enabled_module_names_should_not_recreate_enabled_modules
648 project = Project.find(1)
660 project = Project.find(1)
649 # Remove one module
661 # Remove one module
650 modules = project.enabled_modules.slice(0..-2)
662 modules = project.enabled_modules.slice(0..-2)
651 assert modules.any?
663 assert modules.any?
652 assert_difference 'EnabledModule.count', -1 do
664 assert_difference 'EnabledModule.count', -1 do
653 project.enabled_module_names = modules.collect(&:name)
665 project.enabled_module_names = modules.collect(&:name)
654 end
666 end
655 project.reload
667 project.reload
656 # Ids should be preserved
668 # Ids should be preserved
657 assert_equal project.enabled_module_ids.sort, modules.collect(&:id).sort
669 assert_equal project.enabled_module_ids.sort, modules.collect(&:id).sort
658 end
670 end
659
671
660 def test_copy_from_existing_project
672 def test_copy_from_existing_project
661 source_project = Project.find(1)
673 source_project = Project.find(1)
662 copied_project = Project.copy_from(1)
674 copied_project = Project.copy_from(1)
663
675
664 assert copied_project
676 assert copied_project
665 # Cleared attributes
677 # Cleared attributes
666 assert copied_project.id.blank?
678 assert copied_project.id.blank?
667 assert copied_project.name.blank?
679 assert copied_project.name.blank?
668 assert copied_project.identifier.blank?
680 assert copied_project.identifier.blank?
669
681
670 # Duplicated attributes
682 # Duplicated attributes
671 assert_equal source_project.description, copied_project.description
683 assert_equal source_project.description, copied_project.description
672 assert_equal source_project.enabled_modules, copied_project.enabled_modules
684 assert_equal source_project.enabled_modules, copied_project.enabled_modules
673 assert_equal source_project.trackers, copied_project.trackers
685 assert_equal source_project.trackers, copied_project.trackers
674
686
675 # Default attributes
687 # Default attributes
676 assert_equal 1, copied_project.status
688 assert_equal 1, copied_project.status
677 end
689 end
678
690
679 def test_activities_should_use_the_system_activities
691 def test_activities_should_use_the_system_activities
680 project = Project.find(1)
692 project = Project.find(1)
681 assert_equal project.activities, TimeEntryActivity.find(:all, :conditions => {:active => true} )
693 assert_equal project.activities, TimeEntryActivity.find(:all, :conditions => {:active => true} )
682 end
694 end
683
695
684
696
685 def test_activities_should_use_the_project_specific_activities
697 def test_activities_should_use_the_project_specific_activities
686 project = Project.find(1)
698 project = Project.find(1)
687 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project})
699 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project})
688 assert overridden_activity.save!
700 assert overridden_activity.save!
689
701
690 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
702 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
691 end
703 end
692
704
693 def test_activities_should_not_include_the_inactive_project_specific_activities
705 def test_activities_should_not_include_the_inactive_project_specific_activities
694 project = Project.find(1)
706 project = Project.find(1)
695 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false})
707 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false})
696 assert overridden_activity.save!
708 assert overridden_activity.save!
697
709
698 assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity found"
710 assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity found"
699 end
711 end
700
712
701 def test_activities_should_not_include_project_specific_activities_from_other_projects
713 def test_activities_should_not_include_project_specific_activities_from_other_projects
702 project = Project.find(1)
714 project = Project.find(1)
703 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(2)})
715 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(2)})
704 assert overridden_activity.save!
716 assert overridden_activity.save!
705
717
706 assert !project.activities.include?(overridden_activity), "Project specific Activity found on a different project"
718 assert !project.activities.include?(overridden_activity), "Project specific Activity found on a different project"
707 end
719 end
708
720
709 def test_activities_should_handle_nils
721 def test_activities_should_handle_nils
710 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(1), :parent => TimeEntryActivity.find(:first)})
722 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(1), :parent => TimeEntryActivity.find(:first)})
711 TimeEntryActivity.delete_all
723 TimeEntryActivity.delete_all
712
724
713 # No activities
725 # No activities
714 project = Project.find(1)
726 project = Project.find(1)
715 assert project.activities.empty?
727 assert project.activities.empty?
716
728
717 # No system, one overridden
729 # No system, one overridden
718 assert overridden_activity.save!
730 assert overridden_activity.save!
719 project.reload
731 project.reload
720 assert_equal [overridden_activity], project.activities
732 assert_equal [overridden_activity], project.activities
721 end
733 end
722
734
723 def test_activities_should_override_system_activities_with_project_activities
735 def test_activities_should_override_system_activities_with_project_activities
724 project = Project.find(1)
736 project = Project.find(1)
725 parent_activity = TimeEntryActivity.find(:first)
737 parent_activity = TimeEntryActivity.find(:first)
726 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => parent_activity})
738 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => parent_activity})
727 assert overridden_activity.save!
739 assert overridden_activity.save!
728
740
729 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
741 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
730 assert !project.activities.include?(parent_activity), "System Activity found when it should have been overridden"
742 assert !project.activities.include?(parent_activity), "System Activity found when it should have been overridden"
731 end
743 end
732
744
733 def test_activities_should_include_inactive_activities_if_specified
745 def test_activities_should_include_inactive_activities_if_specified
734 project = Project.find(1)
746 project = Project.find(1)
735 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false})
747 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false})
736 assert overridden_activity.save!
748 assert overridden_activity.save!
737
749
738 assert project.activities(true).include?(overridden_activity), "Inactive Project specific Activity not found"
750 assert project.activities(true).include?(overridden_activity), "Inactive Project specific Activity not found"
739 end
751 end
740
752
741 test 'activities should not include active System activities if the project has an override that is inactive' do
753 test 'activities should not include active System activities if the project has an override that is inactive' do
742 project = Project.find(1)
754 project = Project.find(1)
743 system_activity = TimeEntryActivity.find_by_name('Design')
755 system_activity = TimeEntryActivity.find_by_name('Design')
744 assert system_activity.active?
756 assert system_activity.active?
745 overridden_activity = TimeEntryActivity.generate!(:project => project, :parent => system_activity, :active => false)
757 overridden_activity = TimeEntryActivity.generate!(:project => project, :parent => system_activity, :active => false)
746 assert overridden_activity.save!
758 assert overridden_activity.save!
747
759
748 assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity not found"
760 assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity not found"
749 assert !project.activities.include?(system_activity), "System activity found when the project has an inactive override"
761 assert !project.activities.include?(system_activity), "System activity found when the project has an inactive override"
750 end
762 end
751
763
752 def test_close_completed_versions
764 def test_close_completed_versions
753 Version.update_all("status = 'open'")
765 Version.update_all("status = 'open'")
754 project = Project.find(1)
766 project = Project.find(1)
755 assert_not_nil project.versions.detect {|v| v.completed? && v.status == 'open'}
767 assert_not_nil project.versions.detect {|v| v.completed? && v.status == 'open'}
756 assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'}
768 assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'}
757 project.close_completed_versions
769 project.close_completed_versions
758 project.reload
770 project.reload
759 assert_nil project.versions.detect {|v| v.completed? && v.status != 'closed'}
771 assert_nil project.versions.detect {|v| v.completed? && v.status != 'closed'}
760 assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'}
772 assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'}
761 end
773 end
762
774
763 context "Project#copy" do
775 context "Project#copy" do
764 setup do
776 setup do
765 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
777 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
766 Project.destroy_all :identifier => "copy-test"
778 Project.destroy_all :identifier => "copy-test"
767 @source_project = Project.find(2)
779 @source_project = Project.find(2)
768 @project = Project.new(:name => 'Copy Test', :identifier => 'copy-test')
780 @project = Project.new(:name => 'Copy Test', :identifier => 'copy-test')
769 @project.trackers = @source_project.trackers
781 @project.trackers = @source_project.trackers
770 @project.enabled_module_names = @source_project.enabled_modules.collect(&:name)
782 @project.enabled_module_names = @source_project.enabled_modules.collect(&:name)
771 end
783 end
772
784
773 should "copy issues" do
785 should "copy issues" do
774 @source_project.issues << Issue.generate!(:status => IssueStatus.find_by_name('Closed'),
786 @source_project.issues << Issue.generate!(:status => IssueStatus.find_by_name('Closed'),
775 :subject => "copy issue status",
787 :subject => "copy issue status",
776 :tracker_id => 1,
788 :tracker_id => 1,
777 :assigned_to_id => 2,
789 :assigned_to_id => 2,
778 :project_id => @source_project.id)
790 :project_id => @source_project.id)
779 assert @project.valid?
791 assert @project.valid?
780 assert @project.issues.empty?
792 assert @project.issues.empty?
781 assert @project.copy(@source_project)
793 assert @project.copy(@source_project)
782
794
783 assert_equal @source_project.issues.size, @project.issues.size
795 assert_equal @source_project.issues.size, @project.issues.size
784 @project.issues.each do |issue|
796 @project.issues.each do |issue|
785 assert issue.valid?
797 assert issue.valid?
786 assert ! issue.assigned_to.blank?
798 assert ! issue.assigned_to.blank?
787 assert_equal @project, issue.project
799 assert_equal @project, issue.project
788 end
800 end
789
801
790 copied_issue = @project.issues.first(:conditions => {:subject => "copy issue status"})
802 copied_issue = @project.issues.first(:conditions => {:subject => "copy issue status"})
791 assert copied_issue
803 assert copied_issue
792 assert copied_issue.status
804 assert copied_issue.status
793 assert_equal "Closed", copied_issue.status.name
805 assert_equal "Closed", copied_issue.status.name
794 end
806 end
795
807
796 should "change the new issues to use the copied version" do
808 should "change the new issues to use the copied version" do
797 User.current = User.find(1)
809 User.current = User.find(1)
798 assigned_version = Version.generate!(:name => "Assigned Issues", :status => 'open')
810 assigned_version = Version.generate!(:name => "Assigned Issues", :status => 'open')
799 @source_project.versions << assigned_version
811 @source_project.versions << assigned_version
800 assert_equal 3, @source_project.versions.size
812 assert_equal 3, @source_project.versions.size
801 Issue.generate_for_project!(@source_project,
813 Issue.generate_for_project!(@source_project,
802 :fixed_version_id => assigned_version.id,
814 :fixed_version_id => assigned_version.id,
803 :subject => "change the new issues to use the copied version",
815 :subject => "change the new issues to use the copied version",
804 :tracker_id => 1,
816 :tracker_id => 1,
805 :project_id => @source_project.id)
817 :project_id => @source_project.id)
806
818
807 assert @project.copy(@source_project)
819 assert @project.copy(@source_project)
808 @project.reload
820 @project.reload
809 copied_issue = @project.issues.first(:conditions => {:subject => "change the new issues to use the copied version"})
821 copied_issue = @project.issues.first(:conditions => {:subject => "change the new issues to use the copied version"})
810
822
811 assert copied_issue
823 assert copied_issue
812 assert copied_issue.fixed_version
824 assert copied_issue.fixed_version
813 assert_equal "Assigned Issues", copied_issue.fixed_version.name # Same name
825 assert_equal "Assigned Issues", copied_issue.fixed_version.name # Same name
814 assert_not_equal assigned_version.id, copied_issue.fixed_version.id # Different record
826 assert_not_equal assigned_version.id, copied_issue.fixed_version.id # Different record
815 end
827 end
816
828
817 should "copy issue relations" do
829 should "copy issue relations" do
818 Setting.cross_project_issue_relations = '1'
830 Setting.cross_project_issue_relations = '1'
819
831
820 second_issue = Issue.generate!(:status_id => 5,
832 second_issue = Issue.generate!(:status_id => 5,
821 :subject => "copy issue relation",
833 :subject => "copy issue relation",
822 :tracker_id => 1,
834 :tracker_id => 1,
823 :assigned_to_id => 2,
835 :assigned_to_id => 2,
824 :project_id => @source_project.id)
836 :project_id => @source_project.id)
825 source_relation = IssueRelation.generate!(:issue_from => Issue.find(4),
837 source_relation = IssueRelation.generate!(:issue_from => Issue.find(4),
826 :issue_to => second_issue,
838 :issue_to => second_issue,
827 :relation_type => "relates")
839 :relation_type => "relates")
828 source_relation_cross_project = IssueRelation.generate!(:issue_from => Issue.find(1),
840 source_relation_cross_project = IssueRelation.generate!(:issue_from => Issue.find(1),
829 :issue_to => second_issue,
841 :issue_to => second_issue,
830 :relation_type => "duplicates")
842 :relation_type => "duplicates")
831
843
832 assert @project.copy(@source_project)
844 assert @project.copy(@source_project)
833 assert_equal @source_project.issues.count, @project.issues.count
845 assert_equal @source_project.issues.count, @project.issues.count
834 copied_issue = @project.issues.find_by_subject("Issue on project 2") # Was #4
846 copied_issue = @project.issues.find_by_subject("Issue on project 2") # Was #4
835 copied_second_issue = @project.issues.find_by_subject("copy issue relation")
847 copied_second_issue = @project.issues.find_by_subject("copy issue relation")
836
848
837 # First issue with a relation on project
849 # First issue with a relation on project
838 assert_equal 1, copied_issue.relations.size, "Relation not copied"
850 assert_equal 1, copied_issue.relations.size, "Relation not copied"
839 copied_relation = copied_issue.relations.first
851 copied_relation = copied_issue.relations.first
840 assert_equal "relates", copied_relation.relation_type
852 assert_equal "relates", copied_relation.relation_type
841 assert_equal copied_second_issue.id, copied_relation.issue_to_id
853 assert_equal copied_second_issue.id, copied_relation.issue_to_id
842 assert_not_equal source_relation.id, copied_relation.id
854 assert_not_equal source_relation.id, copied_relation.id
843
855
844 # Second issue with a cross project relation
856 # Second issue with a cross project relation
845 assert_equal 2, copied_second_issue.relations.size, "Relation not copied"
857 assert_equal 2, copied_second_issue.relations.size, "Relation not copied"
846 copied_relation = copied_second_issue.relations.select {|r| r.relation_type == 'duplicates'}.first
858 copied_relation = copied_second_issue.relations.select {|r| r.relation_type == 'duplicates'}.first
847 assert_equal "duplicates", copied_relation.relation_type
859 assert_equal "duplicates", copied_relation.relation_type
848 assert_equal 1, copied_relation.issue_from_id, "Cross project relation not kept"
860 assert_equal 1, copied_relation.issue_from_id, "Cross project relation not kept"
849 assert_not_equal source_relation_cross_project.id, copied_relation.id
861 assert_not_equal source_relation_cross_project.id, copied_relation.id
850 end
862 end
851
863
852 should "copy issue attachments" do
864 should "copy issue attachments" do
853 issue = Issue.generate!(:subject => "copy with attachment", :tracker_id => 1, :project_id => @source_project.id)
865 issue = Issue.generate!(:subject => "copy with attachment", :tracker_id => 1, :project_id => @source_project.id)
854 Attachment.create!(:container => issue, :file => uploaded_test_file("testfile.txt", "text/plain"), :author_id => 1)
866 Attachment.create!(:container => issue, :file => uploaded_test_file("testfile.txt", "text/plain"), :author_id => 1)
855 @source_project.issues << issue
867 @source_project.issues << issue
856 assert @project.copy(@source_project)
868 assert @project.copy(@source_project)
857
869
858 copied_issue = @project.issues.first(:conditions => {:subject => "copy with attachment"})
870 copied_issue = @project.issues.first(:conditions => {:subject => "copy with attachment"})
859 assert_not_nil copied_issue
871 assert_not_nil copied_issue
860 assert_equal 1, copied_issue.attachments.count, "Attachment not copied"
872 assert_equal 1, copied_issue.attachments.count, "Attachment not copied"
861 assert_equal "testfile.txt", copied_issue.attachments.first.filename
873 assert_equal "testfile.txt", copied_issue.attachments.first.filename
862 end
874 end
863
875
864 should "copy memberships" do
876 should "copy memberships" do
865 assert @project.valid?
877 assert @project.valid?
866 assert @project.members.empty?
878 assert @project.members.empty?
867 assert @project.copy(@source_project)
879 assert @project.copy(@source_project)
868
880
869 assert_equal @source_project.memberships.size, @project.memberships.size
881 assert_equal @source_project.memberships.size, @project.memberships.size
870 @project.memberships.each do |membership|
882 @project.memberships.each do |membership|
871 assert membership
883 assert membership
872 assert_equal @project, membership.project
884 assert_equal @project, membership.project
873 end
885 end
874 end
886 end
875
887
876 should "copy memberships with groups and additional roles" do
888 should "copy memberships with groups and additional roles" do
877 group = Group.create!(:lastname => "Copy group")
889 group = Group.create!(:lastname => "Copy group")
878 user = User.find(7)
890 user = User.find(7)
879 group.users << user
891 group.users << user
880 # group role
892 # group role
881 Member.create!(:project_id => @source_project.id, :principal => group, :role_ids => [2])
893 Member.create!(:project_id => @source_project.id, :principal => group, :role_ids => [2])
882 member = Member.find_by_user_id_and_project_id(user.id, @source_project.id)
894 member = Member.find_by_user_id_and_project_id(user.id, @source_project.id)
883 # additional role
895 # additional role
884 member.role_ids = [1]
896 member.role_ids = [1]
885
897
886 assert @project.copy(@source_project)
898 assert @project.copy(@source_project)
887 member = Member.find_by_user_id_and_project_id(user.id, @project.id)
899 member = Member.find_by_user_id_and_project_id(user.id, @project.id)
888 assert_not_nil member
900 assert_not_nil member
889 assert_equal [1, 2], member.role_ids.sort
901 assert_equal [1, 2], member.role_ids.sort
890 end
902 end
891
903
892 should "copy project specific queries" do
904 should "copy project specific queries" do
893 assert @project.valid?
905 assert @project.valid?
894 assert @project.queries.empty?
906 assert @project.queries.empty?
895 assert @project.copy(@source_project)
907 assert @project.copy(@source_project)
896
908
897 assert_equal @source_project.queries.size, @project.queries.size
909 assert_equal @source_project.queries.size, @project.queries.size
898 @project.queries.each do |query|
910 @project.queries.each do |query|
899 assert query
911 assert query
900 assert_equal @project, query.project
912 assert_equal @project, query.project
901 end
913 end
902 assert_equal @source_project.queries.map(&:user_id).sort, @project.queries.map(&:user_id).sort
914 assert_equal @source_project.queries.map(&:user_id).sort, @project.queries.map(&:user_id).sort
903 end
915 end
904
916
905 should "copy versions" do
917 should "copy versions" do
906 @source_project.versions << Version.generate!
918 @source_project.versions << Version.generate!
907 @source_project.versions << Version.generate!
919 @source_project.versions << Version.generate!
908
920
909 assert @project.versions.empty?
921 assert @project.versions.empty?
910 assert @project.copy(@source_project)
922 assert @project.copy(@source_project)
911
923
912 assert_equal @source_project.versions.size, @project.versions.size
924 assert_equal @source_project.versions.size, @project.versions.size
913 @project.versions.each do |version|
925 @project.versions.each do |version|
914 assert version
926 assert version
915 assert_equal @project, version.project
927 assert_equal @project, version.project
916 end
928 end
917 end
929 end
918
930
919 should "copy wiki" do
931 should "copy wiki" do
920 assert_difference 'Wiki.count' do
932 assert_difference 'Wiki.count' do
921 assert @project.copy(@source_project)
933 assert @project.copy(@source_project)
922 end
934 end
923
935
924 assert @project.wiki
936 assert @project.wiki
925 assert_not_equal @source_project.wiki, @project.wiki
937 assert_not_equal @source_project.wiki, @project.wiki
926 assert_equal "Start page", @project.wiki.start_page
938 assert_equal "Start page", @project.wiki.start_page
927 end
939 end
928
940
929 should "copy wiki pages and content with hierarchy" do
941 should "copy wiki pages and content with hierarchy" do
930 assert_difference 'WikiPage.count', @source_project.wiki.pages.size do
942 assert_difference 'WikiPage.count', @source_project.wiki.pages.size do
931 assert @project.copy(@source_project)
943 assert @project.copy(@source_project)
932 end
944 end
933
945
934 assert @project.wiki
946 assert @project.wiki
935 assert_equal @source_project.wiki.pages.size, @project.wiki.pages.size
947 assert_equal @source_project.wiki.pages.size, @project.wiki.pages.size
936
948
937 @project.wiki.pages.each do |wiki_page|
949 @project.wiki.pages.each do |wiki_page|
938 assert wiki_page.content
950 assert wiki_page.content
939 assert !@source_project.wiki.pages.include?(wiki_page)
951 assert !@source_project.wiki.pages.include?(wiki_page)
940 end
952 end
941
953
942 parent = @project.wiki.find_page('Parent_page')
954 parent = @project.wiki.find_page('Parent_page')
943 child1 = @project.wiki.find_page('Child_page_1')
955 child1 = @project.wiki.find_page('Child_page_1')
944 child2 = @project.wiki.find_page('Child_page_2')
956 child2 = @project.wiki.find_page('Child_page_2')
945 assert_equal parent, child1.parent
957 assert_equal parent, child1.parent
946 assert_equal parent, child2.parent
958 assert_equal parent, child2.parent
947 end
959 end
948
960
949 should "copy issue categories" do
961 should "copy issue categories" do
950 assert @project.copy(@source_project)
962 assert @project.copy(@source_project)
951
963
952 assert_equal 2, @project.issue_categories.size
964 assert_equal 2, @project.issue_categories.size
953 @project.issue_categories.each do |issue_category|
965 @project.issue_categories.each do |issue_category|
954 assert !@source_project.issue_categories.include?(issue_category)
966 assert !@source_project.issue_categories.include?(issue_category)
955 end
967 end
956 end
968 end
957
969
958 should "copy boards" do
970 should "copy boards" do
959 assert @project.copy(@source_project)
971 assert @project.copy(@source_project)
960
972
961 assert_equal 1, @project.boards.size
973 assert_equal 1, @project.boards.size
962 @project.boards.each do |board|
974 @project.boards.each do |board|
963 assert !@source_project.boards.include?(board)
975 assert !@source_project.boards.include?(board)
964 end
976 end
965 end
977 end
966
978
967 should "change the new issues to use the copied issue categories" do
979 should "change the new issues to use the copied issue categories" do
968 issue = Issue.find(4)
980 issue = Issue.find(4)
969 issue.update_attribute(:category_id, 3)
981 issue.update_attribute(:category_id, 3)
970
982
971 assert @project.copy(@source_project)
983 assert @project.copy(@source_project)
972
984
973 @project.issues.each do |issue|
985 @project.issues.each do |issue|
974 assert issue.category
986 assert issue.category
975 assert_equal "Stock management", issue.category.name # Same name
987 assert_equal "Stock management", issue.category.name # Same name
976 assert_not_equal IssueCategory.find(3), issue.category # Different record
988 assert_not_equal IssueCategory.find(3), issue.category # Different record
977 end
989 end
978 end
990 end
979
991
980 should "limit copy with :only option" do
992 should "limit copy with :only option" do
981 assert @project.members.empty?
993 assert @project.members.empty?
982 assert @project.issue_categories.empty?
994 assert @project.issue_categories.empty?
983 assert @source_project.issues.any?
995 assert @source_project.issues.any?
984
996
985 assert @project.copy(@source_project, :only => ['members', 'issue_categories'])
997 assert @project.copy(@source_project, :only => ['members', 'issue_categories'])
986
998
987 assert @project.members.any?
999 assert @project.members.any?
988 assert @project.issue_categories.any?
1000 assert @project.issue_categories.any?
989 assert @project.issues.empty?
1001 assert @project.issues.empty?
990 end
1002 end
991
1003
992 end
1004 end
993
1005
994 context "#start_date" do
1006 context "#start_date" do
995 setup do
1007 setup do
996 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
1008 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
997 @project = Project.generate!(:identifier => 'test0')
1009 @project = Project.generate!(:identifier => 'test0')
998 @project.trackers << Tracker.generate!
1010 @project.trackers << Tracker.generate!
999 end
1011 end
1000
1012
1001 should "be nil if there are no issues on the project" do
1013 should "be nil if there are no issues on the project" do
1002 assert_nil @project.start_date
1014 assert_nil @project.start_date
1003 end
1015 end
1004
1016
1005 should "be tested when issues have no start date"
1017 should "be tested when issues have no start date"
1006
1018
1007 should "be the earliest start date of it's issues" do
1019 should "be the earliest start date of it's issues" do
1008 early = 7.days.ago.to_date
1020 early = 7.days.ago.to_date
1009 Issue.generate_for_project!(@project, :start_date => Date.today)
1021 Issue.generate_for_project!(@project, :start_date => Date.today)
1010 Issue.generate_for_project!(@project, :start_date => early)
1022 Issue.generate_for_project!(@project, :start_date => early)
1011
1023
1012 assert_equal early, @project.start_date
1024 assert_equal early, @project.start_date
1013 end
1025 end
1014
1026
1015 end
1027 end
1016
1028
1017 context "#due_date" do
1029 context "#due_date" do
1018 setup do
1030 setup do
1019 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
1031 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
1020 @project = Project.generate!(:identifier => 'test0')
1032 @project = Project.generate!(:identifier => 'test0')
1021 @project.trackers << Tracker.generate!
1033 @project.trackers << Tracker.generate!
1022 end
1034 end
1023
1035
1024 should "be nil if there are no issues on the project" do
1036 should "be nil if there are no issues on the project" do
1025 assert_nil @project.due_date
1037 assert_nil @project.due_date
1026 end
1038 end
1027
1039
1028 should "be tested when issues have no due date"
1040 should "be tested when issues have no due date"
1029
1041
1030 should "be the latest due date of it's issues" do
1042 should "be the latest due date of it's issues" do
1031 future = 7.days.from_now.to_date
1043 future = 7.days.from_now.to_date
1032 Issue.generate_for_project!(@project, :due_date => future)
1044 Issue.generate_for_project!(@project, :due_date => future)
1033 Issue.generate_for_project!(@project, :due_date => Date.today)
1045 Issue.generate_for_project!(@project, :due_date => Date.today)
1034
1046
1035 assert_equal future, @project.due_date
1047 assert_equal future, @project.due_date
1036 end
1048 end
1037
1049
1038 should "be the latest due date of it's versions" do
1050 should "be the latest due date of it's versions" do
1039 future = 7.days.from_now.to_date
1051 future = 7.days.from_now.to_date
1040 @project.versions << Version.generate!(:effective_date => future)
1052 @project.versions << Version.generate!(:effective_date => future)
1041 @project.versions << Version.generate!(:effective_date => Date.today)
1053 @project.versions << Version.generate!(:effective_date => Date.today)
1042
1054
1043
1055
1044 assert_equal future, @project.due_date
1056 assert_equal future, @project.due_date
1045
1057
1046 end
1058 end
1047
1059
1048 should "pick the latest date from it's issues and versions" do
1060 should "pick the latest date from it's issues and versions" do
1049 future = 7.days.from_now.to_date
1061 future = 7.days.from_now.to_date
1050 far_future = 14.days.from_now.to_date
1062 far_future = 14.days.from_now.to_date
1051 Issue.generate_for_project!(@project, :due_date => far_future)
1063 Issue.generate_for_project!(@project, :due_date => far_future)
1052 @project.versions << Version.generate!(:effective_date => future)
1064 @project.versions << Version.generate!(:effective_date => future)
1053
1065
1054 assert_equal far_future, @project.due_date
1066 assert_equal far_future, @project.due_date
1055 end
1067 end
1056
1068
1057 end
1069 end
1058
1070
1059 context "Project#completed_percent" do
1071 context "Project#completed_percent" do
1060 setup do
1072 setup do
1061 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
1073 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
1062 @project = Project.generate!(:identifier => 'test0')
1074 @project = Project.generate!(:identifier => 'test0')
1063 @project.trackers << Tracker.generate!
1075 @project.trackers << Tracker.generate!
1064 end
1076 end
1065
1077
1066 context "no versions" do
1078 context "no versions" do
1067 should "be 100" do
1079 should "be 100" do
1068 assert_equal 100, @project.completed_percent
1080 assert_equal 100, @project.completed_percent
1069 end
1081 end
1070 end
1082 end
1071
1083
1072 context "with versions" do
1084 context "with versions" do
1073 should "return 0 if the versions have no issues" do
1085 should "return 0 if the versions have no issues" do
1074 Version.generate!(:project => @project)
1086 Version.generate!(:project => @project)
1075 Version.generate!(:project => @project)
1087 Version.generate!(:project => @project)
1076
1088
1077 assert_equal 0, @project.completed_percent
1089 assert_equal 0, @project.completed_percent
1078 end
1090 end
1079
1091
1080 should "return 100 if the version has only closed issues" do
1092 should "return 100 if the version has only closed issues" do
1081 v1 = Version.generate!(:project => @project)
1093 v1 = Version.generate!(:project => @project)
1082 Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('Closed'), :fixed_version => v1)
1094 Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('Closed'), :fixed_version => v1)
1083 v2 = Version.generate!(:project => @project)
1095 v2 = Version.generate!(:project => @project)
1084 Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('Closed'), :fixed_version => v2)
1096 Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('Closed'), :fixed_version => v2)
1085
1097
1086 assert_equal 100, @project.completed_percent
1098 assert_equal 100, @project.completed_percent
1087 end
1099 end
1088
1100
1089 should "return the averaged completed percent of the versions (not weighted)" do
1101 should "return the averaged completed percent of the versions (not weighted)" do
1090 v1 = Version.generate!(:project => @project)
1102 v1 = Version.generate!(:project => @project)
1091 Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('New'), :estimated_hours => 10, :done_ratio => 50, :fixed_version => v1)
1103 Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('New'), :estimated_hours => 10, :done_ratio => 50, :fixed_version => v1)
1092 v2 = Version.generate!(:project => @project)
1104 v2 = Version.generate!(:project => @project)
1093 Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('New'), :estimated_hours => 10, :done_ratio => 50, :fixed_version => v2)
1105 Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('New'), :estimated_hours => 10, :done_ratio => 50, :fixed_version => v2)
1094
1106
1095 assert_equal 50, @project.completed_percent
1107 assert_equal 50, @project.completed_percent
1096 end
1108 end
1097
1109
1098 end
1110 end
1099 end
1111 end
1100
1112
1101 context "#notified_users" do
1113 context "#notified_users" do
1102 setup do
1114 setup do
1103 @project = Project.generate!
1115 @project = Project.generate!
1104 @role = Role.generate!
1116 @role = Role.generate!
1105
1117
1106 @user_with_membership_notification = User.generate!(:mail_notification => 'selected')
1118 @user_with_membership_notification = User.generate!(:mail_notification => 'selected')
1107 Member.generate!(:project => @project, :roles => [@role], :principal => @user_with_membership_notification, :mail_notification => true)
1119 Member.generate!(:project => @project, :roles => [@role], :principal => @user_with_membership_notification, :mail_notification => true)
1108
1120
1109 @all_events_user = User.generate!(:mail_notification => 'all')
1121 @all_events_user = User.generate!(:mail_notification => 'all')
1110 Member.generate!(:project => @project, :roles => [@role], :principal => @all_events_user)
1122 Member.generate!(:project => @project, :roles => [@role], :principal => @all_events_user)
1111
1123
1112 @no_events_user = User.generate!(:mail_notification => 'none')
1124 @no_events_user = User.generate!(:mail_notification => 'none')
1113 Member.generate!(:project => @project, :roles => [@role], :principal => @no_events_user)
1125 Member.generate!(:project => @project, :roles => [@role], :principal => @no_events_user)
1114
1126
1115 @only_my_events_user = User.generate!(:mail_notification => 'only_my_events')
1127 @only_my_events_user = User.generate!(:mail_notification => 'only_my_events')
1116 Member.generate!(:project => @project, :roles => [@role], :principal => @only_my_events_user)
1128 Member.generate!(:project => @project, :roles => [@role], :principal => @only_my_events_user)
1117
1129
1118 @only_assigned_user = User.generate!(:mail_notification => 'only_assigned')
1130 @only_assigned_user = User.generate!(:mail_notification => 'only_assigned')
1119 Member.generate!(:project => @project, :roles => [@role], :principal => @only_assigned_user)
1131 Member.generate!(:project => @project, :roles => [@role], :principal => @only_assigned_user)
1120
1132
1121 @only_owned_user = User.generate!(:mail_notification => 'only_owner')
1133 @only_owned_user = User.generate!(:mail_notification => 'only_owner')
1122 Member.generate!(:project => @project, :roles => [@role], :principal => @only_owned_user)
1134 Member.generate!(:project => @project, :roles => [@role], :principal => @only_owned_user)
1123 end
1135 end
1124
1136
1125 should "include members with a mail notification" do
1137 should "include members with a mail notification" do
1126 assert @project.notified_users.include?(@user_with_membership_notification)
1138 assert @project.notified_users.include?(@user_with_membership_notification)
1127 end
1139 end
1128
1140
1129 should "include users with the 'all' notification option" do
1141 should "include users with the 'all' notification option" do
1130 assert @project.notified_users.include?(@all_events_user)
1142 assert @project.notified_users.include?(@all_events_user)
1131 end
1143 end
1132
1144
1133 should "not include users with the 'none' notification option" do
1145 should "not include users with the 'none' notification option" do
1134 assert !@project.notified_users.include?(@no_events_user)
1146 assert !@project.notified_users.include?(@no_events_user)
1135 end
1147 end
1136
1148
1137 should "not include users with the 'only_my_events' notification option" do
1149 should "not include users with the 'only_my_events' notification option" do
1138 assert !@project.notified_users.include?(@only_my_events_user)
1150 assert !@project.notified_users.include?(@only_my_events_user)
1139 end
1151 end
1140
1152
1141 should "not include users with the 'only_assigned' notification option" do
1153 should "not include users with the 'only_assigned' notification option" do
1142 assert !@project.notified_users.include?(@only_assigned_user)
1154 assert !@project.notified_users.include?(@only_assigned_user)
1143 end
1155 end
1144
1156
1145 should "not include users with the 'only_owner' notification option" do
1157 should "not include users with the 'only_owner' notification option" do
1146 assert !@project.notified_users.include?(@only_owned_user)
1158 assert !@project.notified_users.include?(@only_owned_user)
1147 end
1159 end
1148 end
1160 end
1149
1161
1150 end
1162 end
General Comments 0
You need to be logged in to leave comments. Login now