##// END OF EJS Templates
Adds a textual css class for issue priorities (#12216)....
Jean-Philippe Lang -
r10508:113f8b5cf278
parent child
Show More
@@ -0,0 +1,9
1 class AddEnumerationsPositionName < ActiveRecord::Migration
2 def up
3 add_column :enumerations, :position_name, :string, :limit => 30
4 end
5
6 def down
7 remove_column :enumerations, :position_name
8 end
9 end
@@ -0,0 +1,9
1 class PopulateEnumerationsPositionName < ActiveRecord::Migration
2 def up
3 IssuePriority.compute_position_names
4 end
5
6 def down
7 IssuePriority.clear_position_names
8 end
9 end
@@ -1,1365 +1,1365
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 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 :visible_journals,
31 has_many :visible_journals,
32 :class_name => 'Journal',
32 :class_name => 'Journal',
33 :as => :journalized,
33 :as => :journalized,
34 :conditions => Proc.new {
34 :conditions => Proc.new {
35 ["(#{Journal.table_name}.private_notes = ? OR (#{Project.allowed_to_condition(User.current, :view_private_notes)}))", false]
35 ["(#{Journal.table_name}.private_notes = ? OR (#{Project.allowed_to_condition(User.current, :view_private_notes)}))", false]
36 },
36 },
37 :readonly => true
37 :readonly => true
38
38
39 has_many :time_entries, :dependent => :delete_all
39 has_many :time_entries, :dependent => :delete_all
40 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
40 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
41
41
42 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
42 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
43 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
43 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
44
44
45 acts_as_nested_set :scope => 'root_id', :dependent => :destroy
45 acts_as_nested_set :scope => 'root_id', :dependent => :destroy
46 acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed
46 acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed
47 acts_as_customizable
47 acts_as_customizable
48 acts_as_watchable
48 acts_as_watchable
49 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
49 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
50 :include => [:project, :visible_journals],
50 :include => [:project, :visible_journals],
51 # sort by id so that limited eager loading doesn't break with postgresql
51 # sort by id so that limited eager loading doesn't break with postgresql
52 :order_column => "#{table_name}.id"
52 :order_column => "#{table_name}.id"
53 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
53 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
54 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
54 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
55 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
55 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
56
56
57 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
57 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
58 :author_key => :author_id
58 :author_key => :author_id
59
59
60 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
60 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
61
61
62 attr_reader :current_journal
62 attr_reader :current_journal
63 delegate :notes, :notes=, :private_notes, :private_notes=, :to => :current_journal, :allow_nil => true
63 delegate :notes, :notes=, :private_notes, :private_notes=, :to => :current_journal, :allow_nil => true
64
64
65 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
65 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
66
66
67 validates_length_of :subject, :maximum => 255
67 validates_length_of :subject, :maximum => 255
68 validates_inclusion_of :done_ratio, :in => 0..100
68 validates_inclusion_of :done_ratio, :in => 0..100
69 validates_numericality_of :estimated_hours, :allow_nil => true
69 validates_numericality_of :estimated_hours, :allow_nil => true
70 validate :validate_issue, :validate_required_fields
70 validate :validate_issue, :validate_required_fields
71
71
72 scope :visible,
72 scope :visible,
73 lambda {|*args| { :include => :project,
73 lambda {|*args| { :include => :project,
74 :conditions => Issue.visible_condition(args.shift || User.current, *args) } }
74 :conditions => Issue.visible_condition(args.shift || User.current, *args) } }
75
75
76 scope :open, lambda {|*args|
76 scope :open, lambda {|*args|
77 is_closed = args.size > 0 ? !args.first : false
77 is_closed = args.size > 0 ? !args.first : false
78 {:conditions => ["#{IssueStatus.table_name}.is_closed = ?", is_closed], :include => :status}
78 {:conditions => ["#{IssueStatus.table_name}.is_closed = ?", is_closed], :include => :status}
79 }
79 }
80
80
81 scope :recently_updated, :order => "#{Issue.table_name}.updated_on DESC"
81 scope :recently_updated, :order => "#{Issue.table_name}.updated_on DESC"
82 scope :on_active_project, :include => [:status, :project, :tracker],
82 scope :on_active_project, :include => [:status, :project, :tracker],
83 :conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"]
83 :conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"]
84
84
85 before_create :default_assign
85 before_create :default_assign
86 before_save :close_duplicates, :update_done_ratio_from_issue_status, :force_updated_on_change
86 before_save :close_duplicates, :update_done_ratio_from_issue_status, :force_updated_on_change
87 after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?}
87 after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?}
88 after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal
88 after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal
89 # Should be after_create but would be called before previous after_save callbacks
89 # Should be after_create but would be called before previous after_save callbacks
90 after_save :after_create_from_copy
90 after_save :after_create_from_copy
91 after_destroy :update_parent_attributes
91 after_destroy :update_parent_attributes
92
92
93 # Returns a SQL conditions string used to find all issues visible by the specified user
93 # Returns a SQL conditions string used to find all issues visible by the specified user
94 def self.visible_condition(user, options={})
94 def self.visible_condition(user, options={})
95 Project.allowed_to_condition(user, :view_issues, options) do |role, user|
95 Project.allowed_to_condition(user, :view_issues, options) do |role, user|
96 if user.logged?
96 if user.logged?
97 case role.issues_visibility
97 case role.issues_visibility
98 when 'all'
98 when 'all'
99 nil
99 nil
100 when 'default'
100 when 'default'
101 user_ids = [user.id] + user.groups.map(&:id)
101 user_ids = [user.id] + user.groups.map(&:id)
102 "(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
102 "(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
103 when 'own'
103 when 'own'
104 user_ids = [user.id] + user.groups.map(&:id)
104 user_ids = [user.id] + user.groups.map(&:id)
105 "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
105 "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
106 else
106 else
107 '1=0'
107 '1=0'
108 end
108 end
109 else
109 else
110 "(#{table_name}.is_private = #{connection.quoted_false})"
110 "(#{table_name}.is_private = #{connection.quoted_false})"
111 end
111 end
112 end
112 end
113 end
113 end
114
114
115 # Returns true if usr or current user is allowed to view the issue
115 # Returns true if usr or current user is allowed to view the issue
116 def visible?(usr=nil)
116 def visible?(usr=nil)
117 (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
117 (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
118 if user.logged?
118 if user.logged?
119 case role.issues_visibility
119 case role.issues_visibility
120 when 'all'
120 when 'all'
121 true
121 true
122 when 'default'
122 when 'default'
123 !self.is_private? || (self.author == user || user.is_or_belongs_to?(assigned_to))
123 !self.is_private? || (self.author == user || user.is_or_belongs_to?(assigned_to))
124 when 'own'
124 when 'own'
125 self.author == user || user.is_or_belongs_to?(assigned_to)
125 self.author == user || user.is_or_belongs_to?(assigned_to)
126 else
126 else
127 false
127 false
128 end
128 end
129 else
129 else
130 !self.is_private?
130 !self.is_private?
131 end
131 end
132 end
132 end
133 end
133 end
134
134
135 def initialize(attributes=nil, *args)
135 def initialize(attributes=nil, *args)
136 super
136 super
137 if new_record?
137 if new_record?
138 # set default values for new records only
138 # set default values for new records only
139 self.status ||= IssueStatus.default
139 self.status ||= IssueStatus.default
140 self.priority ||= IssuePriority.default
140 self.priority ||= IssuePriority.default
141 self.watcher_user_ids = []
141 self.watcher_user_ids = []
142 end
142 end
143 end
143 end
144
144
145 # AR#Persistence#destroy would raise and RecordNotFound exception
145 # AR#Persistence#destroy would raise and RecordNotFound exception
146 # if the issue was already deleted or updated (non matching lock_version).
146 # if the issue was already deleted or updated (non matching lock_version).
147 # This is a problem when bulk deleting issues or deleting a project
147 # This is a problem when bulk deleting issues or deleting a project
148 # (because an issue may already be deleted if its parent was deleted
148 # (because an issue may already be deleted if its parent was deleted
149 # first).
149 # first).
150 # The issue is reloaded by the nested_set before being deleted so
150 # The issue is reloaded by the nested_set before being deleted so
151 # the lock_version condition should not be an issue but we handle it.
151 # the lock_version condition should not be an issue but we handle it.
152 def destroy
152 def destroy
153 super
153 super
154 rescue ActiveRecord::RecordNotFound
154 rescue ActiveRecord::RecordNotFound
155 # Stale or already deleted
155 # Stale or already deleted
156 begin
156 begin
157 reload
157 reload
158 rescue ActiveRecord::RecordNotFound
158 rescue ActiveRecord::RecordNotFound
159 # The issue was actually already deleted
159 # The issue was actually already deleted
160 @destroyed = true
160 @destroyed = true
161 return freeze
161 return freeze
162 end
162 end
163 # The issue was stale, retry to destroy
163 # The issue was stale, retry to destroy
164 super
164 super
165 end
165 end
166
166
167 def reload(*args)
167 def reload(*args)
168 @workflow_rule_by_attribute = nil
168 @workflow_rule_by_attribute = nil
169 @assignable_versions = nil
169 @assignable_versions = nil
170 super
170 super
171 end
171 end
172
172
173 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
173 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
174 def available_custom_fields
174 def available_custom_fields
175 (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields.all) : []
175 (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields.all) : []
176 end
176 end
177
177
178 # Copies attributes from another issue, arg can be an id or an Issue
178 # Copies attributes from another issue, arg can be an id or an Issue
179 def copy_from(arg, options={})
179 def copy_from(arg, options={})
180 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
180 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
181 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
181 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
182 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
182 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
183 self.status = issue.status
183 self.status = issue.status
184 self.author = User.current
184 self.author = User.current
185 unless options[:attachments] == false
185 unless options[:attachments] == false
186 self.attachments = issue.attachments.map do |attachement|
186 self.attachments = issue.attachments.map do |attachement|
187 attachement.copy(:container => self)
187 attachement.copy(:container => self)
188 end
188 end
189 end
189 end
190 @copied_from = issue
190 @copied_from = issue
191 @copy_options = options
191 @copy_options = options
192 self
192 self
193 end
193 end
194
194
195 # Returns an unsaved copy of the issue
195 # Returns an unsaved copy of the issue
196 def copy(attributes=nil, copy_options={})
196 def copy(attributes=nil, copy_options={})
197 copy = self.class.new.copy_from(self, copy_options)
197 copy = self.class.new.copy_from(self, copy_options)
198 copy.attributes = attributes if attributes
198 copy.attributes = attributes if attributes
199 copy
199 copy
200 end
200 end
201
201
202 # Returns true if the issue is a copy
202 # Returns true if the issue is a copy
203 def copy?
203 def copy?
204 @copied_from.present?
204 @copied_from.present?
205 end
205 end
206
206
207 # Moves/copies an issue to a new project and tracker
207 # Moves/copies an issue to a new project and tracker
208 # Returns the moved/copied issue on success, false on failure
208 # Returns the moved/copied issue on success, false on failure
209 def move_to_project(new_project, new_tracker=nil, options={})
209 def move_to_project(new_project, new_tracker=nil, options={})
210 ActiveSupport::Deprecation.warn "Issue#move_to_project is deprecated, use #project= instead."
210 ActiveSupport::Deprecation.warn "Issue#move_to_project is deprecated, use #project= instead."
211
211
212 if options[:copy]
212 if options[:copy]
213 issue = self.copy
213 issue = self.copy
214 else
214 else
215 issue = self
215 issue = self
216 end
216 end
217
217
218 issue.init_journal(User.current, options[:notes])
218 issue.init_journal(User.current, options[:notes])
219
219
220 # Preserve previous behaviour
220 # Preserve previous behaviour
221 # #move_to_project doesn't change tracker automatically
221 # #move_to_project doesn't change tracker automatically
222 issue.send :project=, new_project, true
222 issue.send :project=, new_project, true
223 if new_tracker
223 if new_tracker
224 issue.tracker = new_tracker
224 issue.tracker = new_tracker
225 end
225 end
226 # Allow bulk setting of attributes on the issue
226 # Allow bulk setting of attributes on the issue
227 if options[:attributes]
227 if options[:attributes]
228 issue.attributes = options[:attributes]
228 issue.attributes = options[:attributes]
229 end
229 end
230
230
231 issue.save ? issue : false
231 issue.save ? issue : false
232 end
232 end
233
233
234 def status_id=(sid)
234 def status_id=(sid)
235 self.status = nil
235 self.status = nil
236 result = write_attribute(:status_id, sid)
236 result = write_attribute(:status_id, sid)
237 @workflow_rule_by_attribute = nil
237 @workflow_rule_by_attribute = nil
238 result
238 result
239 end
239 end
240
240
241 def priority_id=(pid)
241 def priority_id=(pid)
242 self.priority = nil
242 self.priority = nil
243 write_attribute(:priority_id, pid)
243 write_attribute(:priority_id, pid)
244 end
244 end
245
245
246 def category_id=(cid)
246 def category_id=(cid)
247 self.category = nil
247 self.category = nil
248 write_attribute(:category_id, cid)
248 write_attribute(:category_id, cid)
249 end
249 end
250
250
251 def fixed_version_id=(vid)
251 def fixed_version_id=(vid)
252 self.fixed_version = nil
252 self.fixed_version = nil
253 write_attribute(:fixed_version_id, vid)
253 write_attribute(:fixed_version_id, vid)
254 end
254 end
255
255
256 def tracker_id=(tid)
256 def tracker_id=(tid)
257 self.tracker = nil
257 self.tracker = nil
258 result = write_attribute(:tracker_id, tid)
258 result = write_attribute(:tracker_id, tid)
259 @custom_field_values = nil
259 @custom_field_values = nil
260 @workflow_rule_by_attribute = nil
260 @workflow_rule_by_attribute = nil
261 result
261 result
262 end
262 end
263
263
264 def project_id=(project_id)
264 def project_id=(project_id)
265 if project_id.to_s != self.project_id.to_s
265 if project_id.to_s != self.project_id.to_s
266 self.project = (project_id.present? ? Project.find_by_id(project_id) : nil)
266 self.project = (project_id.present? ? Project.find_by_id(project_id) : nil)
267 end
267 end
268 end
268 end
269
269
270 def project=(project, keep_tracker=false)
270 def project=(project, keep_tracker=false)
271 project_was = self.project
271 project_was = self.project
272 write_attribute(:project_id, project ? project.id : nil)
272 write_attribute(:project_id, project ? project.id : nil)
273 association_instance_set('project', project)
273 association_instance_set('project', project)
274 if project_was && project && project_was != project
274 if project_was && project && project_was != project
275 @assignable_versions = nil
275 @assignable_versions = nil
276
276
277 unless keep_tracker || project.trackers.include?(tracker)
277 unless keep_tracker || project.trackers.include?(tracker)
278 self.tracker = project.trackers.first
278 self.tracker = project.trackers.first
279 end
279 end
280 # Reassign to the category with same name if any
280 # Reassign to the category with same name if any
281 if category
281 if category
282 self.category = project.issue_categories.find_by_name(category.name)
282 self.category = project.issue_categories.find_by_name(category.name)
283 end
283 end
284 # Keep the fixed_version if it's still valid in the new_project
284 # Keep the fixed_version if it's still valid in the new_project
285 if fixed_version && fixed_version.project != project && !project.shared_versions.include?(fixed_version)
285 if fixed_version && fixed_version.project != project && !project.shared_versions.include?(fixed_version)
286 self.fixed_version = nil
286 self.fixed_version = nil
287 end
287 end
288 # Clear the parent task if it's no longer valid
288 # Clear the parent task if it's no longer valid
289 unless valid_parent_project?
289 unless valid_parent_project?
290 self.parent_issue_id = nil
290 self.parent_issue_id = nil
291 end
291 end
292 @custom_field_values = nil
292 @custom_field_values = nil
293 end
293 end
294 end
294 end
295
295
296 def description=(arg)
296 def description=(arg)
297 if arg.is_a?(String)
297 if arg.is_a?(String)
298 arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n")
298 arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n")
299 end
299 end
300 write_attribute(:description, arg)
300 write_attribute(:description, arg)
301 end
301 end
302
302
303 # Overrides assign_attributes so that project and tracker get assigned first
303 # Overrides assign_attributes so that project and tracker get assigned first
304 def assign_attributes_with_project_and_tracker_first(new_attributes, *args)
304 def assign_attributes_with_project_and_tracker_first(new_attributes, *args)
305 return if new_attributes.nil?
305 return if new_attributes.nil?
306 attrs = new_attributes.dup
306 attrs = new_attributes.dup
307 attrs.stringify_keys!
307 attrs.stringify_keys!
308
308
309 %w(project project_id tracker tracker_id).each do |attr|
309 %w(project project_id tracker tracker_id).each do |attr|
310 if attrs.has_key?(attr)
310 if attrs.has_key?(attr)
311 send "#{attr}=", attrs.delete(attr)
311 send "#{attr}=", attrs.delete(attr)
312 end
312 end
313 end
313 end
314 send :assign_attributes_without_project_and_tracker_first, attrs, *args
314 send :assign_attributes_without_project_and_tracker_first, attrs, *args
315 end
315 end
316 # Do not redefine alias chain on reload (see #4838)
316 # Do not redefine alias chain on reload (see #4838)
317 alias_method_chain(:assign_attributes, :project_and_tracker_first) unless method_defined?(:assign_attributes_without_project_and_tracker_first)
317 alias_method_chain(:assign_attributes, :project_and_tracker_first) unless method_defined?(:assign_attributes_without_project_and_tracker_first)
318
318
319 def estimated_hours=(h)
319 def estimated_hours=(h)
320 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
320 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
321 end
321 end
322
322
323 safe_attributes 'project_id',
323 safe_attributes 'project_id',
324 :if => lambda {|issue, user|
324 :if => lambda {|issue, user|
325 if issue.new_record?
325 if issue.new_record?
326 issue.copy?
326 issue.copy?
327 elsif user.allowed_to?(:move_issues, issue.project)
327 elsif user.allowed_to?(:move_issues, issue.project)
328 projects = Issue.allowed_target_projects_on_move(user)
328 projects = Issue.allowed_target_projects_on_move(user)
329 projects.include?(issue.project) && projects.size > 1
329 projects.include?(issue.project) && projects.size > 1
330 end
330 end
331 }
331 }
332
332
333 safe_attributes 'tracker_id',
333 safe_attributes 'tracker_id',
334 'status_id',
334 'status_id',
335 'category_id',
335 'category_id',
336 'assigned_to_id',
336 'assigned_to_id',
337 'priority_id',
337 'priority_id',
338 'fixed_version_id',
338 'fixed_version_id',
339 'subject',
339 'subject',
340 'description',
340 'description',
341 'start_date',
341 'start_date',
342 'due_date',
342 'due_date',
343 'done_ratio',
343 'done_ratio',
344 'estimated_hours',
344 'estimated_hours',
345 'custom_field_values',
345 'custom_field_values',
346 'custom_fields',
346 'custom_fields',
347 'lock_version',
347 'lock_version',
348 'notes',
348 'notes',
349 :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
349 :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
350
350
351 safe_attributes 'status_id',
351 safe_attributes 'status_id',
352 'assigned_to_id',
352 'assigned_to_id',
353 'fixed_version_id',
353 'fixed_version_id',
354 'done_ratio',
354 'done_ratio',
355 'lock_version',
355 'lock_version',
356 'notes',
356 'notes',
357 :if => lambda {|issue, user| issue.new_statuses_allowed_to(user).any? }
357 :if => lambda {|issue, user| issue.new_statuses_allowed_to(user).any? }
358
358
359 safe_attributes 'notes',
359 safe_attributes 'notes',
360 :if => lambda {|issue, user| user.allowed_to?(:add_issue_notes, issue.project)}
360 :if => lambda {|issue, user| user.allowed_to?(:add_issue_notes, issue.project)}
361
361
362 safe_attributes 'private_notes',
362 safe_attributes 'private_notes',
363 :if => lambda {|issue, user| !issue.new_record? && user.allowed_to?(:set_notes_private, issue.project)}
363 :if => lambda {|issue, user| !issue.new_record? && user.allowed_to?(:set_notes_private, issue.project)}
364
364
365 safe_attributes 'watcher_user_ids',
365 safe_attributes 'watcher_user_ids',
366 :if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)}
366 :if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)}
367
367
368 safe_attributes 'is_private',
368 safe_attributes 'is_private',
369 :if => lambda {|issue, user|
369 :if => lambda {|issue, user|
370 user.allowed_to?(:set_issues_private, issue.project) ||
370 user.allowed_to?(:set_issues_private, issue.project) ||
371 (issue.author == user && user.allowed_to?(:set_own_issues_private, issue.project))
371 (issue.author == user && user.allowed_to?(:set_own_issues_private, issue.project))
372 }
372 }
373
373
374 safe_attributes 'parent_issue_id',
374 safe_attributes 'parent_issue_id',
375 :if => lambda {|issue, user| (issue.new_record? || user.allowed_to?(:edit_issues, issue.project)) &&
375 :if => lambda {|issue, user| (issue.new_record? || user.allowed_to?(:edit_issues, issue.project)) &&
376 user.allowed_to?(:manage_subtasks, issue.project)}
376 user.allowed_to?(:manage_subtasks, issue.project)}
377
377
378 def safe_attribute_names(user=nil)
378 def safe_attribute_names(user=nil)
379 names = super
379 names = super
380 names -= disabled_core_fields
380 names -= disabled_core_fields
381 names -= read_only_attribute_names(user)
381 names -= read_only_attribute_names(user)
382 names
382 names
383 end
383 end
384
384
385 # Safely sets attributes
385 # Safely sets attributes
386 # Should be called from controllers instead of #attributes=
386 # Should be called from controllers instead of #attributes=
387 # attr_accessible is too rough because we still want things like
387 # attr_accessible is too rough because we still want things like
388 # Issue.new(:project => foo) to work
388 # Issue.new(:project => foo) to work
389 def safe_attributes=(attrs, user=User.current)
389 def safe_attributes=(attrs, user=User.current)
390 return unless attrs.is_a?(Hash)
390 return unless attrs.is_a?(Hash)
391
391
392 attrs = attrs.dup
392 attrs = attrs.dup
393
393
394 # Project and Tracker must be set before since new_statuses_allowed_to depends on it.
394 # Project and Tracker must be set before since new_statuses_allowed_to depends on it.
395 if (p = attrs.delete('project_id')) && safe_attribute?('project_id')
395 if (p = attrs.delete('project_id')) && safe_attribute?('project_id')
396 if allowed_target_projects(user).collect(&:id).include?(p.to_i)
396 if allowed_target_projects(user).collect(&:id).include?(p.to_i)
397 self.project_id = p
397 self.project_id = p
398 end
398 end
399 end
399 end
400
400
401 if (t = attrs.delete('tracker_id')) && safe_attribute?('tracker_id')
401 if (t = attrs.delete('tracker_id')) && safe_attribute?('tracker_id')
402 self.tracker_id = t
402 self.tracker_id = t
403 end
403 end
404
404
405 if (s = attrs.delete('status_id')) && safe_attribute?('status_id')
405 if (s = attrs.delete('status_id')) && safe_attribute?('status_id')
406 if new_statuses_allowed_to(user).collect(&:id).include?(s.to_i)
406 if new_statuses_allowed_to(user).collect(&:id).include?(s.to_i)
407 self.status_id = s
407 self.status_id = s
408 end
408 end
409 end
409 end
410
410
411 attrs = delete_unsafe_attributes(attrs, user)
411 attrs = delete_unsafe_attributes(attrs, user)
412 return if attrs.empty?
412 return if attrs.empty?
413
413
414 unless leaf?
414 unless leaf?
415 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
415 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
416 end
416 end
417
417
418 if attrs['parent_issue_id'].present?
418 if attrs['parent_issue_id'].present?
419 s = attrs['parent_issue_id'].to_s
419 s = attrs['parent_issue_id'].to_s
420 unless (m = s.match(%r{\A#?(\d+)\z})) && Issue.visible(user).exists?(m[1])
420 unless (m = s.match(%r{\A#?(\d+)\z})) && Issue.visible(user).exists?(m[1])
421 @invalid_parent_issue_id = attrs.delete('parent_issue_id')
421 @invalid_parent_issue_id = attrs.delete('parent_issue_id')
422 end
422 end
423 end
423 end
424
424
425 if attrs['custom_field_values'].present?
425 if attrs['custom_field_values'].present?
426 attrs['custom_field_values'] = attrs['custom_field_values'].reject {|k, v| read_only_attribute_names(user).include? k.to_s}
426 attrs['custom_field_values'] = attrs['custom_field_values'].reject {|k, v| read_only_attribute_names(user).include? k.to_s}
427 end
427 end
428
428
429 if attrs['custom_fields'].present?
429 if attrs['custom_fields'].present?
430 attrs['custom_fields'] = attrs['custom_fields'].reject {|c| read_only_attribute_names(user).include? c['id'].to_s}
430 attrs['custom_fields'] = attrs['custom_fields'].reject {|c| read_only_attribute_names(user).include? c['id'].to_s}
431 end
431 end
432
432
433 # mass-assignment security bypass
433 # mass-assignment security bypass
434 assign_attributes attrs, :without_protection => true
434 assign_attributes attrs, :without_protection => true
435 end
435 end
436
436
437 def disabled_core_fields
437 def disabled_core_fields
438 tracker ? tracker.disabled_core_fields : []
438 tracker ? tracker.disabled_core_fields : []
439 end
439 end
440
440
441 # Returns the custom_field_values that can be edited by the given user
441 # Returns the custom_field_values that can be edited by the given user
442 def editable_custom_field_values(user=nil)
442 def editable_custom_field_values(user=nil)
443 custom_field_values.reject do |value|
443 custom_field_values.reject do |value|
444 read_only_attribute_names(user).include?(value.custom_field_id.to_s)
444 read_only_attribute_names(user).include?(value.custom_field_id.to_s)
445 end
445 end
446 end
446 end
447
447
448 # Returns the names of attributes that are read-only for user or the current user
448 # Returns the names of attributes that are read-only for user or the current user
449 # For users with multiple roles, the read-only fields are the intersection of
449 # For users with multiple roles, the read-only fields are the intersection of
450 # read-only fields of each role
450 # read-only fields of each role
451 # The result is an array of strings where sustom fields are represented with their ids
451 # The result is an array of strings where sustom fields are represented with their ids
452 #
452 #
453 # Examples:
453 # Examples:
454 # issue.read_only_attribute_names # => ['due_date', '2']
454 # issue.read_only_attribute_names # => ['due_date', '2']
455 # issue.read_only_attribute_names(user) # => []
455 # issue.read_only_attribute_names(user) # => []
456 def read_only_attribute_names(user=nil)
456 def read_only_attribute_names(user=nil)
457 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'readonly'}.keys
457 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'readonly'}.keys
458 end
458 end
459
459
460 # Returns the names of required attributes for user or the current user
460 # Returns the names of required attributes for user or the current user
461 # For users with multiple roles, the required fields are the intersection of
461 # For users with multiple roles, the required fields are the intersection of
462 # required fields of each role
462 # required fields of each role
463 # The result is an array of strings where sustom fields are represented with their ids
463 # The result is an array of strings where sustom fields are represented with their ids
464 #
464 #
465 # Examples:
465 # Examples:
466 # issue.required_attribute_names # => ['due_date', '2']
466 # issue.required_attribute_names # => ['due_date', '2']
467 # issue.required_attribute_names(user) # => []
467 # issue.required_attribute_names(user) # => []
468 def required_attribute_names(user=nil)
468 def required_attribute_names(user=nil)
469 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'required'}.keys
469 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'required'}.keys
470 end
470 end
471
471
472 # Returns true if the attribute is required for user
472 # Returns true if the attribute is required for user
473 def required_attribute?(name, user=nil)
473 def required_attribute?(name, user=nil)
474 required_attribute_names(user).include?(name.to_s)
474 required_attribute_names(user).include?(name.to_s)
475 end
475 end
476
476
477 # Returns a hash of the workflow rule by attribute for the given user
477 # Returns a hash of the workflow rule by attribute for the given user
478 #
478 #
479 # Examples:
479 # Examples:
480 # issue.workflow_rule_by_attribute # => {'due_date' => 'required', 'start_date' => 'readonly'}
480 # issue.workflow_rule_by_attribute # => {'due_date' => 'required', 'start_date' => 'readonly'}
481 def workflow_rule_by_attribute(user=nil)
481 def workflow_rule_by_attribute(user=nil)
482 return @workflow_rule_by_attribute if @workflow_rule_by_attribute && user.nil?
482 return @workflow_rule_by_attribute if @workflow_rule_by_attribute && user.nil?
483
483
484 user_real = user || User.current
484 user_real = user || User.current
485 roles = user_real.admin ? Role.all : user_real.roles_for_project(project)
485 roles = user_real.admin ? Role.all : user_real.roles_for_project(project)
486 return {} if roles.empty?
486 return {} if roles.empty?
487
487
488 result = {}
488 result = {}
489 workflow_permissions = WorkflowPermission.where(:tracker_id => tracker_id, :old_status_id => status_id, :role_id => roles.map(&:id)).all
489 workflow_permissions = WorkflowPermission.where(:tracker_id => tracker_id, :old_status_id => status_id, :role_id => roles.map(&:id)).all
490 if workflow_permissions.any?
490 if workflow_permissions.any?
491 workflow_rules = workflow_permissions.inject({}) do |h, wp|
491 workflow_rules = workflow_permissions.inject({}) do |h, wp|
492 h[wp.field_name] ||= []
492 h[wp.field_name] ||= []
493 h[wp.field_name] << wp.rule
493 h[wp.field_name] << wp.rule
494 h
494 h
495 end
495 end
496 workflow_rules.each do |attr, rules|
496 workflow_rules.each do |attr, rules|
497 next if rules.size < roles.size
497 next if rules.size < roles.size
498 uniq_rules = rules.uniq
498 uniq_rules = rules.uniq
499 if uniq_rules.size == 1
499 if uniq_rules.size == 1
500 result[attr] = uniq_rules.first
500 result[attr] = uniq_rules.first
501 else
501 else
502 result[attr] = 'required'
502 result[attr] = 'required'
503 end
503 end
504 end
504 end
505 end
505 end
506 @workflow_rule_by_attribute = result if user.nil?
506 @workflow_rule_by_attribute = result if user.nil?
507 result
507 result
508 end
508 end
509 private :workflow_rule_by_attribute
509 private :workflow_rule_by_attribute
510
510
511 def done_ratio
511 def done_ratio
512 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
512 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
513 status.default_done_ratio
513 status.default_done_ratio
514 else
514 else
515 read_attribute(:done_ratio)
515 read_attribute(:done_ratio)
516 end
516 end
517 end
517 end
518
518
519 def self.use_status_for_done_ratio?
519 def self.use_status_for_done_ratio?
520 Setting.issue_done_ratio == 'issue_status'
520 Setting.issue_done_ratio == 'issue_status'
521 end
521 end
522
522
523 def self.use_field_for_done_ratio?
523 def self.use_field_for_done_ratio?
524 Setting.issue_done_ratio == 'issue_field'
524 Setting.issue_done_ratio == 'issue_field'
525 end
525 end
526
526
527 def validate_issue
527 def validate_issue
528 if due_date.nil? && @attributes['due_date'].present?
528 if due_date.nil? && @attributes['due_date'].present?
529 errors.add :due_date, :not_a_date
529 errors.add :due_date, :not_a_date
530 end
530 end
531
531
532 if start_date.nil? && @attributes['start_date'].present?
532 if start_date.nil? && @attributes['start_date'].present?
533 errors.add :start_date, :not_a_date
533 errors.add :start_date, :not_a_date
534 end
534 end
535
535
536 if due_date && start_date && due_date < start_date
536 if due_date && start_date && due_date < start_date
537 errors.add :due_date, :greater_than_start_date
537 errors.add :due_date, :greater_than_start_date
538 end
538 end
539
539
540 if start_date && soonest_start && start_date < soonest_start
540 if start_date && soonest_start && start_date < soonest_start
541 errors.add :start_date, :invalid
541 errors.add :start_date, :invalid
542 end
542 end
543
543
544 if fixed_version
544 if fixed_version
545 if !assignable_versions.include?(fixed_version)
545 if !assignable_versions.include?(fixed_version)
546 errors.add :fixed_version_id, :inclusion
546 errors.add :fixed_version_id, :inclusion
547 elsif reopened? && fixed_version.closed?
547 elsif reopened? && fixed_version.closed?
548 errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version)
548 errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version)
549 end
549 end
550 end
550 end
551
551
552 # Checks that the issue can not be added/moved to a disabled tracker
552 # Checks that the issue can not be added/moved to a disabled tracker
553 if project && (tracker_id_changed? || project_id_changed?)
553 if project && (tracker_id_changed? || project_id_changed?)
554 unless project.trackers.include?(tracker)
554 unless project.trackers.include?(tracker)
555 errors.add :tracker_id, :inclusion
555 errors.add :tracker_id, :inclusion
556 end
556 end
557 end
557 end
558
558
559 # Checks parent issue assignment
559 # Checks parent issue assignment
560 if @invalid_parent_issue_id.present?
560 if @invalid_parent_issue_id.present?
561 errors.add :parent_issue_id, :invalid
561 errors.add :parent_issue_id, :invalid
562 elsif @parent_issue
562 elsif @parent_issue
563 if !valid_parent_project?(@parent_issue)
563 if !valid_parent_project?(@parent_issue)
564 errors.add :parent_issue_id, :invalid
564 errors.add :parent_issue_id, :invalid
565 elsif !new_record?
565 elsif !new_record?
566 # moving an existing issue
566 # moving an existing issue
567 if @parent_issue.root_id != root_id
567 if @parent_issue.root_id != root_id
568 # we can always move to another tree
568 # we can always move to another tree
569 elsif move_possible?(@parent_issue)
569 elsif move_possible?(@parent_issue)
570 # move accepted inside tree
570 # move accepted inside tree
571 else
571 else
572 errors.add :parent_issue_id, :invalid
572 errors.add :parent_issue_id, :invalid
573 end
573 end
574 end
574 end
575 end
575 end
576 end
576 end
577
577
578 # Validates the issue against additional workflow requirements
578 # Validates the issue against additional workflow requirements
579 def validate_required_fields
579 def validate_required_fields
580 user = new_record? ? author : current_journal.try(:user)
580 user = new_record? ? author : current_journal.try(:user)
581
581
582 required_attribute_names(user).each do |attribute|
582 required_attribute_names(user).each do |attribute|
583 if attribute =~ /^\d+$/
583 if attribute =~ /^\d+$/
584 attribute = attribute.to_i
584 attribute = attribute.to_i
585 v = custom_field_values.detect {|v| v.custom_field_id == attribute }
585 v = custom_field_values.detect {|v| v.custom_field_id == attribute }
586 if v && v.value.blank?
586 if v && v.value.blank?
587 errors.add :base, v.custom_field.name + ' ' + l('activerecord.errors.messages.blank')
587 errors.add :base, v.custom_field.name + ' ' + l('activerecord.errors.messages.blank')
588 end
588 end
589 else
589 else
590 if respond_to?(attribute) && send(attribute).blank?
590 if respond_to?(attribute) && send(attribute).blank?
591 errors.add attribute, :blank
591 errors.add attribute, :blank
592 end
592 end
593 end
593 end
594 end
594 end
595 end
595 end
596
596
597 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
597 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
598 # even if the user turns off the setting later
598 # even if the user turns off the setting later
599 def update_done_ratio_from_issue_status
599 def update_done_ratio_from_issue_status
600 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
600 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
601 self.done_ratio = status.default_done_ratio
601 self.done_ratio = status.default_done_ratio
602 end
602 end
603 end
603 end
604
604
605 def init_journal(user, notes = "")
605 def init_journal(user, notes = "")
606 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
606 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
607 if new_record?
607 if new_record?
608 @current_journal.notify = false
608 @current_journal.notify = false
609 else
609 else
610 @attributes_before_change = attributes.dup
610 @attributes_before_change = attributes.dup
611 @custom_values_before_change = {}
611 @custom_values_before_change = {}
612 self.custom_field_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
612 self.custom_field_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
613 end
613 end
614 @current_journal
614 @current_journal
615 end
615 end
616
616
617 # Returns the id of the last journal or nil
617 # Returns the id of the last journal or nil
618 def last_journal_id
618 def last_journal_id
619 if new_record?
619 if new_record?
620 nil
620 nil
621 else
621 else
622 journals.maximum(:id)
622 journals.maximum(:id)
623 end
623 end
624 end
624 end
625
625
626 # Returns a scope for journals that have an id greater than journal_id
626 # Returns a scope for journals that have an id greater than journal_id
627 def journals_after(journal_id)
627 def journals_after(journal_id)
628 scope = journals.reorder("#{Journal.table_name}.id ASC")
628 scope = journals.reorder("#{Journal.table_name}.id ASC")
629 if journal_id.present?
629 if journal_id.present?
630 scope = scope.where("#{Journal.table_name}.id > ?", journal_id.to_i)
630 scope = scope.where("#{Journal.table_name}.id > ?", journal_id.to_i)
631 end
631 end
632 scope
632 scope
633 end
633 end
634
634
635 # Return true if the issue is closed, otherwise false
635 # Return true if the issue is closed, otherwise false
636 def closed?
636 def closed?
637 self.status.is_closed?
637 self.status.is_closed?
638 end
638 end
639
639
640 # Return true if the issue is being reopened
640 # Return true if the issue is being reopened
641 def reopened?
641 def reopened?
642 if !new_record? && status_id_changed?
642 if !new_record? && status_id_changed?
643 status_was = IssueStatus.find_by_id(status_id_was)
643 status_was = IssueStatus.find_by_id(status_id_was)
644 status_new = IssueStatus.find_by_id(status_id)
644 status_new = IssueStatus.find_by_id(status_id)
645 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
645 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
646 return true
646 return true
647 end
647 end
648 end
648 end
649 false
649 false
650 end
650 end
651
651
652 # Return true if the issue is being closed
652 # Return true if the issue is being closed
653 def closing?
653 def closing?
654 if !new_record? && status_id_changed?
654 if !new_record? && status_id_changed?
655 status_was = IssueStatus.find_by_id(status_id_was)
655 status_was = IssueStatus.find_by_id(status_id_was)
656 status_new = IssueStatus.find_by_id(status_id)
656 status_new = IssueStatus.find_by_id(status_id)
657 if status_was && status_new && !status_was.is_closed? && status_new.is_closed?
657 if status_was && status_new && !status_was.is_closed? && status_new.is_closed?
658 return true
658 return true
659 end
659 end
660 end
660 end
661 false
661 false
662 end
662 end
663
663
664 # Returns true if the issue is overdue
664 # Returns true if the issue is overdue
665 def overdue?
665 def overdue?
666 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
666 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
667 end
667 end
668
668
669 # Is the amount of work done less than it should for the due date
669 # Is the amount of work done less than it should for the due date
670 def behind_schedule?
670 def behind_schedule?
671 return false if start_date.nil? || due_date.nil?
671 return false if start_date.nil? || due_date.nil?
672 done_date = start_date + ((due_date - start_date+1)* done_ratio/100).floor
672 done_date = start_date + ((due_date - start_date+1)* done_ratio/100).floor
673 return done_date <= Date.today
673 return done_date <= Date.today
674 end
674 end
675
675
676 # Does this issue have children?
676 # Does this issue have children?
677 def children?
677 def children?
678 !leaf?
678 !leaf?
679 end
679 end
680
680
681 # Users the issue can be assigned to
681 # Users the issue can be assigned to
682 def assignable_users
682 def assignable_users
683 users = project.assignable_users
683 users = project.assignable_users
684 users << author if author
684 users << author if author
685 users << assigned_to if assigned_to
685 users << assigned_to if assigned_to
686 users.uniq.sort
686 users.uniq.sort
687 end
687 end
688
688
689 # Versions that the issue can be assigned to
689 # Versions that the issue can be assigned to
690 def assignable_versions
690 def assignable_versions
691 return @assignable_versions if @assignable_versions
691 return @assignable_versions if @assignable_versions
692
692
693 versions = project.shared_versions.open.all
693 versions = project.shared_versions.open.all
694 if fixed_version
694 if fixed_version
695 if fixed_version_id_changed?
695 if fixed_version_id_changed?
696 # nothing to do
696 # nothing to do
697 elsif project_id_changed?
697 elsif project_id_changed?
698 if project.shared_versions.include?(fixed_version)
698 if project.shared_versions.include?(fixed_version)
699 versions << fixed_version
699 versions << fixed_version
700 end
700 end
701 else
701 else
702 versions << fixed_version
702 versions << fixed_version
703 end
703 end
704 end
704 end
705 @assignable_versions = versions.uniq.sort
705 @assignable_versions = versions.uniq.sort
706 end
706 end
707
707
708 # Returns true if this issue is blocked by another issue that is still open
708 # Returns true if this issue is blocked by another issue that is still open
709 def blocked?
709 def blocked?
710 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
710 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
711 end
711 end
712
712
713 # Returns an array of statuses that user is able to apply
713 # Returns an array of statuses that user is able to apply
714 def new_statuses_allowed_to(user=User.current, include_default=false)
714 def new_statuses_allowed_to(user=User.current, include_default=false)
715 if new_record? && @copied_from
715 if new_record? && @copied_from
716 [IssueStatus.default, @copied_from.status].compact.uniq.sort
716 [IssueStatus.default, @copied_from.status].compact.uniq.sort
717 else
717 else
718 initial_status = nil
718 initial_status = nil
719 if new_record?
719 if new_record?
720 initial_status = IssueStatus.default
720 initial_status = IssueStatus.default
721 elsif status_id_was
721 elsif status_id_was
722 initial_status = IssueStatus.find_by_id(status_id_was)
722 initial_status = IssueStatus.find_by_id(status_id_was)
723 end
723 end
724 initial_status ||= status
724 initial_status ||= status
725
725
726 statuses = initial_status.find_new_statuses_allowed_to(
726 statuses = initial_status.find_new_statuses_allowed_to(
727 user.admin ? Role.all : user.roles_for_project(project),
727 user.admin ? Role.all : user.roles_for_project(project),
728 tracker,
728 tracker,
729 author == user,
729 author == user,
730 assigned_to_id_changed? ? assigned_to_id_was == user.id : assigned_to_id == user.id
730 assigned_to_id_changed? ? assigned_to_id_was == user.id : assigned_to_id == user.id
731 )
731 )
732 statuses << initial_status unless statuses.empty?
732 statuses << initial_status unless statuses.empty?
733 statuses << IssueStatus.default if include_default
733 statuses << IssueStatus.default if include_default
734 statuses = statuses.compact.uniq.sort
734 statuses = statuses.compact.uniq.sort
735 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
735 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
736 end
736 end
737 end
737 end
738
738
739 def assigned_to_was
739 def assigned_to_was
740 if assigned_to_id_changed? && assigned_to_id_was.present?
740 if assigned_to_id_changed? && assigned_to_id_was.present?
741 @assigned_to_was ||= User.find_by_id(assigned_to_id_was)
741 @assigned_to_was ||= User.find_by_id(assigned_to_id_was)
742 end
742 end
743 end
743 end
744
744
745 # Returns the users that should be notified
745 # Returns the users that should be notified
746 def notified_users
746 def notified_users
747 notified = []
747 notified = []
748 # Author and assignee are always notified unless they have been
748 # Author and assignee are always notified unless they have been
749 # locked or don't want to be notified
749 # locked or don't want to be notified
750 notified << author if author
750 notified << author if author
751 if assigned_to
751 if assigned_to
752 notified += (assigned_to.is_a?(Group) ? assigned_to.users : [assigned_to])
752 notified += (assigned_to.is_a?(Group) ? assigned_to.users : [assigned_to])
753 end
753 end
754 if assigned_to_was
754 if assigned_to_was
755 notified += (assigned_to_was.is_a?(Group) ? assigned_to_was.users : [assigned_to_was])
755 notified += (assigned_to_was.is_a?(Group) ? assigned_to_was.users : [assigned_to_was])
756 end
756 end
757 notified = notified.select {|u| u.active? && u.notify_about?(self)}
757 notified = notified.select {|u| u.active? && u.notify_about?(self)}
758
758
759 notified += project.notified_users
759 notified += project.notified_users
760 notified.uniq!
760 notified.uniq!
761 # Remove users that can not view the issue
761 # Remove users that can not view the issue
762 notified.reject! {|user| !visible?(user)}
762 notified.reject! {|user| !visible?(user)}
763 notified
763 notified
764 end
764 end
765
765
766 # Returns the email addresses that should be notified
766 # Returns the email addresses that should be notified
767 def recipients
767 def recipients
768 notified_users.collect(&:mail)
768 notified_users.collect(&:mail)
769 end
769 end
770
770
771 # Returns the number of hours spent on this issue
771 # Returns the number of hours spent on this issue
772 def spent_hours
772 def spent_hours
773 @spent_hours ||= time_entries.sum(:hours) || 0
773 @spent_hours ||= time_entries.sum(:hours) || 0
774 end
774 end
775
775
776 # Returns the total number of hours spent on this issue and its descendants
776 # Returns the total number of hours spent on this issue and its descendants
777 #
777 #
778 # Example:
778 # Example:
779 # spent_hours => 0.0
779 # spent_hours => 0.0
780 # spent_hours => 50.2
780 # spent_hours => 50.2
781 def total_spent_hours
781 def total_spent_hours
782 @total_spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours",
782 @total_spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours",
783 :joins => "LEFT JOIN #{TimeEntry.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id").to_f || 0.0
783 :joins => "LEFT JOIN #{TimeEntry.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id").to_f || 0.0
784 end
784 end
785
785
786 def relations
786 def relations
787 @relations ||= IssueRelations.new(self, (relations_from + relations_to).sort)
787 @relations ||= IssueRelations.new(self, (relations_from + relations_to).sort)
788 end
788 end
789
789
790 # Preloads relations for a collection of issues
790 # Preloads relations for a collection of issues
791 def self.load_relations(issues)
791 def self.load_relations(issues)
792 if issues.any?
792 if issues.any?
793 relations = IssueRelation.all(:conditions => ["issue_from_id IN (:ids) OR issue_to_id IN (:ids)", {:ids => issues.map(&:id)}])
793 relations = IssueRelation.all(:conditions => ["issue_from_id IN (:ids) OR issue_to_id IN (:ids)", {:ids => issues.map(&:id)}])
794 issues.each do |issue|
794 issues.each do |issue|
795 issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
795 issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
796 end
796 end
797 end
797 end
798 end
798 end
799
799
800 # Preloads visible spent time for a collection of issues
800 # Preloads visible spent time for a collection of issues
801 def self.load_visible_spent_hours(issues, user=User.current)
801 def self.load_visible_spent_hours(issues, user=User.current)
802 if issues.any?
802 if issues.any?
803 hours_by_issue_id = TimeEntry.visible(user).sum(:hours, :group => :issue_id)
803 hours_by_issue_id = TimeEntry.visible(user).sum(:hours, :group => :issue_id)
804 issues.each do |issue|
804 issues.each do |issue|
805 issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0)
805 issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0)
806 end
806 end
807 end
807 end
808 end
808 end
809
809
810 # Preloads visible relations for a collection of issues
810 # Preloads visible relations for a collection of issues
811 def self.load_visible_relations(issues, user=User.current)
811 def self.load_visible_relations(issues, user=User.current)
812 if issues.any?
812 if issues.any?
813 issue_ids = issues.map(&:id)
813 issue_ids = issues.map(&:id)
814 # Relations with issue_from in given issues and visible issue_to
814 # Relations with issue_from in given issues and visible issue_to
815 relations_from = IssueRelation.includes(:issue_to => [:status, :project]).where(visible_condition(user)).where(:issue_from_id => issue_ids).all
815 relations_from = IssueRelation.includes(:issue_to => [:status, :project]).where(visible_condition(user)).where(:issue_from_id => issue_ids).all
816 # Relations with issue_to in given issues and visible issue_from
816 # Relations with issue_to in given issues and visible issue_from
817 relations_to = IssueRelation.includes(:issue_from => [:status, :project]).where(visible_condition(user)).where(:issue_to_id => issue_ids).all
817 relations_to = IssueRelation.includes(:issue_from => [:status, :project]).where(visible_condition(user)).where(:issue_to_id => issue_ids).all
818
818
819 issues.each do |issue|
819 issues.each do |issue|
820 relations =
820 relations =
821 relations_from.select {|relation| relation.issue_from_id == issue.id} +
821 relations_from.select {|relation| relation.issue_from_id == issue.id} +
822 relations_to.select {|relation| relation.issue_to_id == issue.id}
822 relations_to.select {|relation| relation.issue_to_id == issue.id}
823
823
824 issue.instance_variable_set "@relations", IssueRelations.new(issue, relations.sort)
824 issue.instance_variable_set "@relations", IssueRelations.new(issue, relations.sort)
825 end
825 end
826 end
826 end
827 end
827 end
828
828
829 # Finds an issue relation given its id.
829 # Finds an issue relation given its id.
830 def find_relation(relation_id)
830 def find_relation(relation_id)
831 IssueRelation.find(relation_id, :conditions => ["issue_to_id = ? OR issue_from_id = ?", id, id])
831 IssueRelation.find(relation_id, :conditions => ["issue_to_id = ? OR issue_from_id = ?", id, id])
832 end
832 end
833
833
834 def all_dependent_issues(except=[])
834 def all_dependent_issues(except=[])
835 except << self
835 except << self
836 dependencies = []
836 dependencies = []
837 relations_from.each do |relation|
837 relations_from.each do |relation|
838 if relation.issue_to && !except.include?(relation.issue_to)
838 if relation.issue_to && !except.include?(relation.issue_to)
839 dependencies << relation.issue_to
839 dependencies << relation.issue_to
840 dependencies += relation.issue_to.all_dependent_issues(except)
840 dependencies += relation.issue_to.all_dependent_issues(except)
841 end
841 end
842 end
842 end
843 dependencies
843 dependencies
844 end
844 end
845
845
846 # Returns an array of issues that duplicate this one
846 # Returns an array of issues that duplicate this one
847 def duplicates
847 def duplicates
848 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
848 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
849 end
849 end
850
850
851 # Returns the due date or the target due date if any
851 # Returns the due date or the target due date if any
852 # Used on gantt chart
852 # Used on gantt chart
853 def due_before
853 def due_before
854 due_date || (fixed_version ? fixed_version.effective_date : nil)
854 due_date || (fixed_version ? fixed_version.effective_date : nil)
855 end
855 end
856
856
857 # Returns the time scheduled for this issue.
857 # Returns the time scheduled for this issue.
858 #
858 #
859 # Example:
859 # Example:
860 # Start Date: 2/26/09, End Date: 3/04/09
860 # Start Date: 2/26/09, End Date: 3/04/09
861 # duration => 6
861 # duration => 6
862 def duration
862 def duration
863 (start_date && due_date) ? due_date - start_date : 0
863 (start_date && due_date) ? due_date - start_date : 0
864 end
864 end
865
865
866 def soonest_start
866 def soonest_start
867 @soonest_start ||= (
867 @soonest_start ||= (
868 relations_to.collect{|relation| relation.successor_soonest_start} +
868 relations_to.collect{|relation| relation.successor_soonest_start} +
869 ancestors.collect(&:soonest_start)
869 ancestors.collect(&:soonest_start)
870 ).compact.max
870 ).compact.max
871 end
871 end
872
872
873 def reschedule_after(date)
873 def reschedule_after(date)
874 return if date.nil?
874 return if date.nil?
875 if leaf?
875 if leaf?
876 if start_date.nil? || start_date < date
876 if start_date.nil? || start_date < date
877 self.start_date, self.due_date = date, date + duration
877 self.start_date, self.due_date = date, date + duration
878 begin
878 begin
879 save
879 save
880 rescue ActiveRecord::StaleObjectError
880 rescue ActiveRecord::StaleObjectError
881 reload
881 reload
882 self.start_date, self.due_date = date, date + duration
882 self.start_date, self.due_date = date, date + duration
883 save
883 save
884 end
884 end
885 end
885 end
886 else
886 else
887 leaves.each do |leaf|
887 leaves.each do |leaf|
888 leaf.reschedule_after(date)
888 leaf.reschedule_after(date)
889 end
889 end
890 end
890 end
891 end
891 end
892
892
893 def <=>(issue)
893 def <=>(issue)
894 if issue.nil?
894 if issue.nil?
895 -1
895 -1
896 elsif root_id != issue.root_id
896 elsif root_id != issue.root_id
897 (root_id || 0) <=> (issue.root_id || 0)
897 (root_id || 0) <=> (issue.root_id || 0)
898 else
898 else
899 (lft || 0) <=> (issue.lft || 0)
899 (lft || 0) <=> (issue.lft || 0)
900 end
900 end
901 end
901 end
902
902
903 def to_s
903 def to_s
904 "#{tracker} ##{id}: #{subject}"
904 "#{tracker} ##{id}: #{subject}"
905 end
905 end
906
906
907 # Returns a string of css classes that apply to the issue
907 # Returns a string of css classes that apply to the issue
908 def css_classes
908 def css_classes
909 s = "issue status-#{status_id} priority-#{priority_id}"
909 s = "issue status-#{status_id} #{priority.try(:css_classes)}"
910 s << ' closed' if closed?
910 s << ' closed' if closed?
911 s << ' overdue' if overdue?
911 s << ' overdue' if overdue?
912 s << ' child' if child?
912 s << ' child' if child?
913 s << ' parent' unless leaf?
913 s << ' parent' unless leaf?
914 s << ' private' if is_private?
914 s << ' private' if is_private?
915 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
915 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
916 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
916 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
917 s
917 s
918 end
918 end
919
919
920 # Saves an issue and a time_entry from the parameters
920 # Saves an issue and a time_entry from the parameters
921 def save_issue_with_child_records(params, existing_time_entry=nil)
921 def save_issue_with_child_records(params, existing_time_entry=nil)
922 Issue.transaction do
922 Issue.transaction do
923 if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, project)
923 if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, project)
924 @time_entry = existing_time_entry || TimeEntry.new
924 @time_entry = existing_time_entry || TimeEntry.new
925 @time_entry.project = project
925 @time_entry.project = project
926 @time_entry.issue = self
926 @time_entry.issue = self
927 @time_entry.user = User.current
927 @time_entry.user = User.current
928 @time_entry.spent_on = User.current.today
928 @time_entry.spent_on = User.current.today
929 @time_entry.attributes = params[:time_entry]
929 @time_entry.attributes = params[:time_entry]
930 self.time_entries << @time_entry
930 self.time_entries << @time_entry
931 end
931 end
932
932
933 # TODO: Rename hook
933 # TODO: Rename hook
934 Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
934 Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
935 if save
935 if save
936 # TODO: Rename hook
936 # TODO: Rename hook
937 Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
937 Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
938 else
938 else
939 raise ActiveRecord::Rollback
939 raise ActiveRecord::Rollback
940 end
940 end
941 end
941 end
942 end
942 end
943
943
944 # Unassigns issues from +version+ if it's no longer shared with issue's project
944 # Unassigns issues from +version+ if it's no longer shared with issue's project
945 def self.update_versions_from_sharing_change(version)
945 def self.update_versions_from_sharing_change(version)
946 # Update issues assigned to the version
946 # Update issues assigned to the version
947 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
947 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
948 end
948 end
949
949
950 # Unassigns issues from versions that are no longer shared
950 # Unassigns issues from versions that are no longer shared
951 # after +project+ was moved
951 # after +project+ was moved
952 def self.update_versions_from_hierarchy_change(project)
952 def self.update_versions_from_hierarchy_change(project)
953 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
953 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
954 # Update issues of the moved projects and issues assigned to a version of a moved project
954 # Update issues of the moved projects and issues assigned to a version of a moved project
955 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
955 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
956 end
956 end
957
957
958 def parent_issue_id=(arg)
958 def parent_issue_id=(arg)
959 s = arg.to_s.strip.presence
959 s = arg.to_s.strip.presence
960 if s && (m = s.match(%r{\A#?(\d+)\z})) && (@parent_issue = Issue.find_by_id(m[1]))
960 if s && (m = s.match(%r{\A#?(\d+)\z})) && (@parent_issue = Issue.find_by_id(m[1]))
961 @parent_issue.id
961 @parent_issue.id
962 else
962 else
963 @parent_issue = nil
963 @parent_issue = nil
964 @invalid_parent_issue_id = arg
964 @invalid_parent_issue_id = arg
965 end
965 end
966 end
966 end
967
967
968 def parent_issue_id
968 def parent_issue_id
969 if @invalid_parent_issue_id
969 if @invalid_parent_issue_id
970 @invalid_parent_issue_id
970 @invalid_parent_issue_id
971 elsif instance_variable_defined? :@parent_issue
971 elsif instance_variable_defined? :@parent_issue
972 @parent_issue.nil? ? nil : @parent_issue.id
972 @parent_issue.nil? ? nil : @parent_issue.id
973 else
973 else
974 parent_id
974 parent_id
975 end
975 end
976 end
976 end
977
977
978 # Returns true if issue's project is a valid
978 # Returns true if issue's project is a valid
979 # parent issue project
979 # parent issue project
980 def valid_parent_project?(issue=parent)
980 def valid_parent_project?(issue=parent)
981 return true if issue.nil? || issue.project_id == project_id
981 return true if issue.nil? || issue.project_id == project_id
982
982
983 case Setting.cross_project_subtasks
983 case Setting.cross_project_subtasks
984 when 'system'
984 when 'system'
985 true
985 true
986 when 'tree'
986 when 'tree'
987 issue.project.root == project.root
987 issue.project.root == project.root
988 when 'hierarchy'
988 when 'hierarchy'
989 issue.project.is_or_is_ancestor_of?(project) || issue.project.is_descendant_of?(project)
989 issue.project.is_or_is_ancestor_of?(project) || issue.project.is_descendant_of?(project)
990 when 'descendants'
990 when 'descendants'
991 issue.project.is_or_is_ancestor_of?(project)
991 issue.project.is_or_is_ancestor_of?(project)
992 else
992 else
993 false
993 false
994 end
994 end
995 end
995 end
996
996
997 # Extracted from the ReportsController.
997 # Extracted from the ReportsController.
998 def self.by_tracker(project)
998 def self.by_tracker(project)
999 count_and_group_by(:project => project,
999 count_and_group_by(:project => project,
1000 :field => 'tracker_id',
1000 :field => 'tracker_id',
1001 :joins => Tracker.table_name)
1001 :joins => Tracker.table_name)
1002 end
1002 end
1003
1003
1004 def self.by_version(project)
1004 def self.by_version(project)
1005 count_and_group_by(:project => project,
1005 count_and_group_by(:project => project,
1006 :field => 'fixed_version_id',
1006 :field => 'fixed_version_id',
1007 :joins => Version.table_name)
1007 :joins => Version.table_name)
1008 end
1008 end
1009
1009
1010 def self.by_priority(project)
1010 def self.by_priority(project)
1011 count_and_group_by(:project => project,
1011 count_and_group_by(:project => project,
1012 :field => 'priority_id',
1012 :field => 'priority_id',
1013 :joins => IssuePriority.table_name)
1013 :joins => IssuePriority.table_name)
1014 end
1014 end
1015
1015
1016 def self.by_category(project)
1016 def self.by_category(project)
1017 count_and_group_by(:project => project,
1017 count_and_group_by(:project => project,
1018 :field => 'category_id',
1018 :field => 'category_id',
1019 :joins => IssueCategory.table_name)
1019 :joins => IssueCategory.table_name)
1020 end
1020 end
1021
1021
1022 def self.by_assigned_to(project)
1022 def self.by_assigned_to(project)
1023 count_and_group_by(:project => project,
1023 count_and_group_by(:project => project,
1024 :field => 'assigned_to_id',
1024 :field => 'assigned_to_id',
1025 :joins => User.table_name)
1025 :joins => User.table_name)
1026 end
1026 end
1027
1027
1028 def self.by_author(project)
1028 def self.by_author(project)
1029 count_and_group_by(:project => project,
1029 count_and_group_by(:project => project,
1030 :field => 'author_id',
1030 :field => 'author_id',
1031 :joins => User.table_name)
1031 :joins => User.table_name)
1032 end
1032 end
1033
1033
1034 def self.by_subproject(project)
1034 def self.by_subproject(project)
1035 ActiveRecord::Base.connection.select_all("select s.id as status_id,
1035 ActiveRecord::Base.connection.select_all("select s.id as status_id,
1036 s.is_closed as closed,
1036 s.is_closed as closed,
1037 #{Issue.table_name}.project_id as project_id,
1037 #{Issue.table_name}.project_id as project_id,
1038 count(#{Issue.table_name}.id) as total
1038 count(#{Issue.table_name}.id) as total
1039 from
1039 from
1040 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s
1040 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s
1041 where
1041 where
1042 #{Issue.table_name}.status_id=s.id
1042 #{Issue.table_name}.status_id=s.id
1043 and #{Issue.table_name}.project_id = #{Project.table_name}.id
1043 and #{Issue.table_name}.project_id = #{Project.table_name}.id
1044 and #{visible_condition(User.current, :project => project, :with_subprojects => true)}
1044 and #{visible_condition(User.current, :project => project, :with_subprojects => true)}
1045 and #{Issue.table_name}.project_id <> #{project.id}
1045 and #{Issue.table_name}.project_id <> #{project.id}
1046 group by s.id, s.is_closed, #{Issue.table_name}.project_id") if project.descendants.active.any?
1046 group by s.id, s.is_closed, #{Issue.table_name}.project_id") if project.descendants.active.any?
1047 end
1047 end
1048 # End ReportsController extraction
1048 # End ReportsController extraction
1049
1049
1050 # Returns an array of projects that user can assign the issue to
1050 # Returns an array of projects that user can assign the issue to
1051 def allowed_target_projects(user=User.current)
1051 def allowed_target_projects(user=User.current)
1052 if new_record?
1052 if new_record?
1053 Project.all(:conditions => Project.allowed_to_condition(user, :add_issues))
1053 Project.all(:conditions => Project.allowed_to_condition(user, :add_issues))
1054 else
1054 else
1055 self.class.allowed_target_projects_on_move(user)
1055 self.class.allowed_target_projects_on_move(user)
1056 end
1056 end
1057 end
1057 end
1058
1058
1059 # Returns an array of projects that user can move issues to
1059 # Returns an array of projects that user can move issues to
1060 def self.allowed_target_projects_on_move(user=User.current)
1060 def self.allowed_target_projects_on_move(user=User.current)
1061 Project.all(:conditions => Project.allowed_to_condition(user, :move_issues))
1061 Project.all(:conditions => Project.allowed_to_condition(user, :move_issues))
1062 end
1062 end
1063
1063
1064 private
1064 private
1065
1065
1066 def after_project_change
1066 def after_project_change
1067 # Update project_id on related time entries
1067 # Update project_id on related time entries
1068 TimeEntry.update_all(["project_id = ?", project_id], {:issue_id => id})
1068 TimeEntry.update_all(["project_id = ?", project_id], {:issue_id => id})
1069
1069
1070 # Delete issue relations
1070 # Delete issue relations
1071 unless Setting.cross_project_issue_relations?
1071 unless Setting.cross_project_issue_relations?
1072 relations_from.clear
1072 relations_from.clear
1073 relations_to.clear
1073 relations_to.clear
1074 end
1074 end
1075
1075
1076 # Move subtasks that were in the same project
1076 # Move subtasks that were in the same project
1077 children.each do |child|
1077 children.each do |child|
1078 next unless child.project_id == project_id_was
1078 next unless child.project_id == project_id_was
1079 # Change project and keep project
1079 # Change project and keep project
1080 child.send :project=, project, true
1080 child.send :project=, project, true
1081 unless child.save
1081 unless child.save
1082 raise ActiveRecord::Rollback
1082 raise ActiveRecord::Rollback
1083 end
1083 end
1084 end
1084 end
1085 end
1085 end
1086
1086
1087 # Callback for after the creation of an issue by copy
1087 # Callback for after the creation of an issue by copy
1088 # * adds a "copied to" relation with the copied issue
1088 # * adds a "copied to" relation with the copied issue
1089 # * copies subtasks from the copied issue
1089 # * copies subtasks from the copied issue
1090 def after_create_from_copy
1090 def after_create_from_copy
1091 return unless copy? && !@after_create_from_copy_handled
1091 return unless copy? && !@after_create_from_copy_handled
1092
1092
1093 if (@copied_from.project_id == project_id || Setting.cross_project_issue_relations?) && @copy_options[:link] != false
1093 if (@copied_from.project_id == project_id || Setting.cross_project_issue_relations?) && @copy_options[:link] != false
1094 relation = IssueRelation.new(:issue_from => @copied_from, :issue_to => self, :relation_type => IssueRelation::TYPE_COPIED_TO)
1094 relation = IssueRelation.new(:issue_from => @copied_from, :issue_to => self, :relation_type => IssueRelation::TYPE_COPIED_TO)
1095 unless relation.save
1095 unless relation.save
1096 logger.error "Could not create relation while copying ##{@copied_from.id} to ##{id} due to validation errors: #{relation.errors.full_messages.join(', ')}" if logger
1096 logger.error "Could not create relation while copying ##{@copied_from.id} to ##{id} due to validation errors: #{relation.errors.full_messages.join(', ')}" if logger
1097 end
1097 end
1098 end
1098 end
1099
1099
1100 unless @copied_from.leaf? || @copy_options[:subtasks] == false
1100 unless @copied_from.leaf? || @copy_options[:subtasks] == false
1101 @copied_from.children.each do |child|
1101 @copied_from.children.each do |child|
1102 unless child.visible?
1102 unless child.visible?
1103 # Do not copy subtasks that are not visible to avoid potential disclosure of private data
1103 # Do not copy subtasks that are not visible to avoid potential disclosure of private data
1104 logger.error "Subtask ##{child.id} was not copied during ##{@copied_from.id} copy because it is not visible to the current user" if logger
1104 logger.error "Subtask ##{child.id} was not copied during ##{@copied_from.id} copy because it is not visible to the current user" if logger
1105 next
1105 next
1106 end
1106 end
1107 copy = Issue.new.copy_from(child, @copy_options)
1107 copy = Issue.new.copy_from(child, @copy_options)
1108 copy.author = author
1108 copy.author = author
1109 copy.project = project
1109 copy.project = project
1110 copy.parent_issue_id = id
1110 copy.parent_issue_id = id
1111 # Children subtasks are copied recursively
1111 # Children subtasks are copied recursively
1112 unless copy.save
1112 unless copy.save
1113 logger.error "Could not copy subtask ##{child.id} while copying ##{@copied_from.id} to ##{id} due to validation errors: #{copy.errors.full_messages.join(', ')}" if logger
1113 logger.error "Could not copy subtask ##{child.id} while copying ##{@copied_from.id} to ##{id} due to validation errors: #{copy.errors.full_messages.join(', ')}" if logger
1114 end
1114 end
1115 end
1115 end
1116 end
1116 end
1117 @after_create_from_copy_handled = true
1117 @after_create_from_copy_handled = true
1118 end
1118 end
1119
1119
1120 def update_nested_set_attributes
1120 def update_nested_set_attributes
1121 if root_id.nil?
1121 if root_id.nil?
1122 # issue was just created
1122 # issue was just created
1123 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
1123 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
1124 set_default_left_and_right
1124 set_default_left_and_right
1125 Issue.update_all("root_id = #{root_id}, lft = #{lft}, rgt = #{rgt}", ["id = ?", id])
1125 Issue.update_all("root_id = #{root_id}, lft = #{lft}, rgt = #{rgt}", ["id = ?", id])
1126 if @parent_issue
1126 if @parent_issue
1127 move_to_child_of(@parent_issue)
1127 move_to_child_of(@parent_issue)
1128 end
1128 end
1129 reload
1129 reload
1130 elsif parent_issue_id != parent_id
1130 elsif parent_issue_id != parent_id
1131 former_parent_id = parent_id
1131 former_parent_id = parent_id
1132 # moving an existing issue
1132 # moving an existing issue
1133 if @parent_issue && @parent_issue.root_id == root_id
1133 if @parent_issue && @parent_issue.root_id == root_id
1134 # inside the same tree
1134 # inside the same tree
1135 move_to_child_of(@parent_issue)
1135 move_to_child_of(@parent_issue)
1136 else
1136 else
1137 # to another tree
1137 # to another tree
1138 unless root?
1138 unless root?
1139 move_to_right_of(root)
1139 move_to_right_of(root)
1140 reload
1140 reload
1141 end
1141 end
1142 old_root_id = root_id
1142 old_root_id = root_id
1143 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
1143 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
1144 target_maxright = nested_set_scope.maximum(right_column_name) || 0
1144 target_maxright = nested_set_scope.maximum(right_column_name) || 0
1145 offset = target_maxright + 1 - lft
1145 offset = target_maxright + 1 - lft
1146 Issue.update_all("root_id = #{root_id}, lft = lft + #{offset}, rgt = rgt + #{offset}",
1146 Issue.update_all("root_id = #{root_id}, lft = lft + #{offset}, rgt = rgt + #{offset}",
1147 ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
1147 ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
1148 self[left_column_name] = lft + offset
1148 self[left_column_name] = lft + offset
1149 self[right_column_name] = rgt + offset
1149 self[right_column_name] = rgt + offset
1150 if @parent_issue
1150 if @parent_issue
1151 move_to_child_of(@parent_issue)
1151 move_to_child_of(@parent_issue)
1152 end
1152 end
1153 end
1153 end
1154 reload
1154 reload
1155 # delete invalid relations of all descendants
1155 # delete invalid relations of all descendants
1156 self_and_descendants.each do |issue|
1156 self_and_descendants.each do |issue|
1157 issue.relations.each do |relation|
1157 issue.relations.each do |relation|
1158 relation.destroy unless relation.valid?
1158 relation.destroy unless relation.valid?
1159 end
1159 end
1160 end
1160 end
1161 # update former parent
1161 # update former parent
1162 recalculate_attributes_for(former_parent_id) if former_parent_id
1162 recalculate_attributes_for(former_parent_id) if former_parent_id
1163 end
1163 end
1164 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
1164 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
1165 end
1165 end
1166
1166
1167 def update_parent_attributes
1167 def update_parent_attributes
1168 recalculate_attributes_for(parent_id) if parent_id
1168 recalculate_attributes_for(parent_id) if parent_id
1169 end
1169 end
1170
1170
1171 def recalculate_attributes_for(issue_id)
1171 def recalculate_attributes_for(issue_id)
1172 if issue_id && p = Issue.find_by_id(issue_id)
1172 if issue_id && p = Issue.find_by_id(issue_id)
1173 # priority = highest priority of children
1173 # priority = highest priority of children
1174 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :joins => :priority)
1174 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :joins => :priority)
1175 p.priority = IssuePriority.find_by_position(priority_position)
1175 p.priority = IssuePriority.find_by_position(priority_position)
1176 end
1176 end
1177
1177
1178 # start/due dates = lowest/highest dates of children
1178 # start/due dates = lowest/highest dates of children
1179 p.start_date = p.children.minimum(:start_date)
1179 p.start_date = p.children.minimum(:start_date)
1180 p.due_date = p.children.maximum(:due_date)
1180 p.due_date = p.children.maximum(:due_date)
1181 if p.start_date && p.due_date && p.due_date < p.start_date
1181 if p.start_date && p.due_date && p.due_date < p.start_date
1182 p.start_date, p.due_date = p.due_date, p.start_date
1182 p.start_date, p.due_date = p.due_date, p.start_date
1183 end
1183 end
1184
1184
1185 # done ratio = weighted average ratio of leaves
1185 # done ratio = weighted average ratio of leaves
1186 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
1186 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
1187 leaves_count = p.leaves.count
1187 leaves_count = p.leaves.count
1188 if leaves_count > 0
1188 if leaves_count > 0
1189 average = p.leaves.average(:estimated_hours).to_f
1189 average = p.leaves.average(:estimated_hours).to_f
1190 if average == 0
1190 if average == 0
1191 average = 1
1191 average = 1
1192 end
1192 end
1193 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
1193 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
1194 progress = done / (average * leaves_count)
1194 progress = done / (average * leaves_count)
1195 p.done_ratio = progress.round
1195 p.done_ratio = progress.round
1196 end
1196 end
1197 end
1197 end
1198
1198
1199 # estimate = sum of leaves estimates
1199 # estimate = sum of leaves estimates
1200 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
1200 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
1201 p.estimated_hours = nil if p.estimated_hours == 0.0
1201 p.estimated_hours = nil if p.estimated_hours == 0.0
1202
1202
1203 # ancestors will be recursively updated
1203 # ancestors will be recursively updated
1204 p.save(:validate => false)
1204 p.save(:validate => false)
1205 end
1205 end
1206 end
1206 end
1207
1207
1208 # Update issues so their versions are not pointing to a
1208 # Update issues so their versions are not pointing to a
1209 # fixed_version that is not shared with the issue's project
1209 # fixed_version that is not shared with the issue's project
1210 def self.update_versions(conditions=nil)
1210 def self.update_versions(conditions=nil)
1211 # Only need to update issues with a fixed_version from
1211 # Only need to update issues with a fixed_version from
1212 # a different project and that is not systemwide shared
1212 # a different project and that is not systemwide shared
1213 Issue.scoped(:conditions => conditions).all(
1213 Issue.scoped(:conditions => conditions).all(
1214 :conditions => "#{Issue.table_name}.fixed_version_id IS NOT NULL" +
1214 :conditions => "#{Issue.table_name}.fixed_version_id IS NOT NULL" +
1215 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
1215 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
1216 " AND #{Version.table_name}.sharing <> 'system'",
1216 " AND #{Version.table_name}.sharing <> 'system'",
1217 :include => [:project, :fixed_version]
1217 :include => [:project, :fixed_version]
1218 ).each do |issue|
1218 ).each do |issue|
1219 next if issue.project.nil? || issue.fixed_version.nil?
1219 next if issue.project.nil? || issue.fixed_version.nil?
1220 unless issue.project.shared_versions.include?(issue.fixed_version)
1220 unless issue.project.shared_versions.include?(issue.fixed_version)
1221 issue.init_journal(User.current)
1221 issue.init_journal(User.current)
1222 issue.fixed_version = nil
1222 issue.fixed_version = nil
1223 issue.save
1223 issue.save
1224 end
1224 end
1225 end
1225 end
1226 end
1226 end
1227
1227
1228 # Callback on file attachment
1228 # Callback on file attachment
1229 def attachment_added(obj)
1229 def attachment_added(obj)
1230 if @current_journal && !obj.new_record?
1230 if @current_journal && !obj.new_record?
1231 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :value => obj.filename)
1231 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :value => obj.filename)
1232 end
1232 end
1233 end
1233 end
1234
1234
1235 # Callback on attachment deletion
1235 # Callback on attachment deletion
1236 def attachment_removed(obj)
1236 def attachment_removed(obj)
1237 if @current_journal && !obj.new_record?
1237 if @current_journal && !obj.new_record?
1238 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :old_value => obj.filename)
1238 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :old_value => obj.filename)
1239 @current_journal.save
1239 @current_journal.save
1240 end
1240 end
1241 end
1241 end
1242
1242
1243 # Default assignment based on category
1243 # Default assignment based on category
1244 def default_assign
1244 def default_assign
1245 if assigned_to.nil? && category && category.assigned_to
1245 if assigned_to.nil? && category && category.assigned_to
1246 self.assigned_to = category.assigned_to
1246 self.assigned_to = category.assigned_to
1247 end
1247 end
1248 end
1248 end
1249
1249
1250 # Updates start/due dates of following issues
1250 # Updates start/due dates of following issues
1251 def reschedule_following_issues
1251 def reschedule_following_issues
1252 if start_date_changed? || due_date_changed?
1252 if start_date_changed? || due_date_changed?
1253 relations_from.each do |relation|
1253 relations_from.each do |relation|
1254 relation.set_issue_to_dates
1254 relation.set_issue_to_dates
1255 end
1255 end
1256 end
1256 end
1257 end
1257 end
1258
1258
1259 # Closes duplicates if the issue is being closed
1259 # Closes duplicates if the issue is being closed
1260 def close_duplicates
1260 def close_duplicates
1261 if closing?
1261 if closing?
1262 duplicates.each do |duplicate|
1262 duplicates.each do |duplicate|
1263 # Reload is need in case the duplicate was updated by a previous duplicate
1263 # Reload is need in case the duplicate was updated by a previous duplicate
1264 duplicate.reload
1264 duplicate.reload
1265 # Don't re-close it if it's already closed
1265 # Don't re-close it if it's already closed
1266 next if duplicate.closed?
1266 next if duplicate.closed?
1267 # Same user and notes
1267 # Same user and notes
1268 if @current_journal
1268 if @current_journal
1269 duplicate.init_journal(@current_journal.user, @current_journal.notes)
1269 duplicate.init_journal(@current_journal.user, @current_journal.notes)
1270 end
1270 end
1271 duplicate.update_attribute :status, self.status
1271 duplicate.update_attribute :status, self.status
1272 end
1272 end
1273 end
1273 end
1274 end
1274 end
1275
1275
1276 # Make sure updated_on is updated when adding a note
1276 # Make sure updated_on is updated when adding a note
1277 def force_updated_on_change
1277 def force_updated_on_change
1278 if @current_journal
1278 if @current_journal
1279 self.updated_on = current_time_from_proper_timezone
1279 self.updated_on = current_time_from_proper_timezone
1280 end
1280 end
1281 end
1281 end
1282
1282
1283 # Saves the changes in a Journal
1283 # Saves the changes in a Journal
1284 # Called after_save
1284 # Called after_save
1285 def create_journal
1285 def create_journal
1286 if @current_journal
1286 if @current_journal
1287 # attributes changes
1287 # attributes changes
1288 if @attributes_before_change
1288 if @attributes_before_change
1289 (Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on)).each {|c|
1289 (Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on)).each {|c|
1290 before = @attributes_before_change[c]
1290 before = @attributes_before_change[c]
1291 after = send(c)
1291 after = send(c)
1292 next if before == after || (before.blank? && after.blank?)
1292 next if before == after || (before.blank? && after.blank?)
1293 @current_journal.details << JournalDetail.new(:property => 'attr',
1293 @current_journal.details << JournalDetail.new(:property => 'attr',
1294 :prop_key => c,
1294 :prop_key => c,
1295 :old_value => before,
1295 :old_value => before,
1296 :value => after)
1296 :value => after)
1297 }
1297 }
1298 end
1298 end
1299 if @custom_values_before_change
1299 if @custom_values_before_change
1300 # custom fields changes
1300 # custom fields changes
1301 custom_field_values.each {|c|
1301 custom_field_values.each {|c|
1302 before = @custom_values_before_change[c.custom_field_id]
1302 before = @custom_values_before_change[c.custom_field_id]
1303 after = c.value
1303 after = c.value
1304 next if before == after || (before.blank? && after.blank?)
1304 next if before == after || (before.blank? && after.blank?)
1305
1305
1306 if before.is_a?(Array) || after.is_a?(Array)
1306 if before.is_a?(Array) || after.is_a?(Array)
1307 before = [before] unless before.is_a?(Array)
1307 before = [before] unless before.is_a?(Array)
1308 after = [after] unless after.is_a?(Array)
1308 after = [after] unless after.is_a?(Array)
1309
1309
1310 # values removed
1310 # values removed
1311 (before - after).reject(&:blank?).each do |value|
1311 (before - after).reject(&:blank?).each do |value|
1312 @current_journal.details << JournalDetail.new(:property => 'cf',
1312 @current_journal.details << JournalDetail.new(:property => 'cf',
1313 :prop_key => c.custom_field_id,
1313 :prop_key => c.custom_field_id,
1314 :old_value => value,
1314 :old_value => value,
1315 :value => nil)
1315 :value => nil)
1316 end
1316 end
1317 # values added
1317 # values added
1318 (after - before).reject(&:blank?).each do |value|
1318 (after - before).reject(&:blank?).each do |value|
1319 @current_journal.details << JournalDetail.new(:property => 'cf',
1319 @current_journal.details << JournalDetail.new(:property => 'cf',
1320 :prop_key => c.custom_field_id,
1320 :prop_key => c.custom_field_id,
1321 :old_value => nil,
1321 :old_value => nil,
1322 :value => value)
1322 :value => value)
1323 end
1323 end
1324 else
1324 else
1325 @current_journal.details << JournalDetail.new(:property => 'cf',
1325 @current_journal.details << JournalDetail.new(:property => 'cf',
1326 :prop_key => c.custom_field_id,
1326 :prop_key => c.custom_field_id,
1327 :old_value => before,
1327 :old_value => before,
1328 :value => after)
1328 :value => after)
1329 end
1329 end
1330 }
1330 }
1331 end
1331 end
1332 @current_journal.save
1332 @current_journal.save
1333 # reset current journal
1333 # reset current journal
1334 init_journal @current_journal.user, @current_journal.notes
1334 init_journal @current_journal.user, @current_journal.notes
1335 end
1335 end
1336 end
1336 end
1337
1337
1338 # Query generator for selecting groups of issue counts for a project
1338 # Query generator for selecting groups of issue counts for a project
1339 # based on specific criteria
1339 # based on specific criteria
1340 #
1340 #
1341 # Options
1341 # Options
1342 # * project - Project to search in.
1342 # * project - Project to search in.
1343 # * field - String. Issue field to key off of in the grouping.
1343 # * field - String. Issue field to key off of in the grouping.
1344 # * joins - String. The table name to join against.
1344 # * joins - String. The table name to join against.
1345 def self.count_and_group_by(options)
1345 def self.count_and_group_by(options)
1346 project = options.delete(:project)
1346 project = options.delete(:project)
1347 select_field = options.delete(:field)
1347 select_field = options.delete(:field)
1348 joins = options.delete(:joins)
1348 joins = options.delete(:joins)
1349
1349
1350 where = "#{Issue.table_name}.#{select_field}=j.id"
1350 where = "#{Issue.table_name}.#{select_field}=j.id"
1351
1351
1352 ActiveRecord::Base.connection.select_all("select s.id as status_id,
1352 ActiveRecord::Base.connection.select_all("select s.id as status_id,
1353 s.is_closed as closed,
1353 s.is_closed as closed,
1354 j.id as #{select_field},
1354 j.id as #{select_field},
1355 count(#{Issue.table_name}.id) as total
1355 count(#{Issue.table_name}.id) as total
1356 from
1356 from
1357 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s, #{joins} j
1357 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s, #{joins} j
1358 where
1358 where
1359 #{Issue.table_name}.status_id=s.id
1359 #{Issue.table_name}.status_id=s.id
1360 and #{where}
1360 and #{where}
1361 and #{Issue.table_name}.project_id=#{Project.table_name}.id
1361 and #{Issue.table_name}.project_id=#{Project.table_name}.id
1362 and #{visible_condition(User.current, :project => project)}
1362 and #{visible_condition(User.current, :project => project)}
1363 group by s.id, s.is_closed, j.id")
1363 group by s.id, s.is_closed, j.id")
1364 end
1364 end
1365 end
1365 end
@@ -1,34 +1,68
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 class IssuePriority < Enumeration
18 class IssuePriority < Enumeration
19 has_many :issues, :foreign_key => 'priority_id'
19 has_many :issues, :foreign_key => 'priority_id'
20
20
21 after_destroy {|priority| priority.class.compute_position_names}
22 after_save {|priority| priority.class.compute_position_names if priority.position_changed? && priority.position}
23
21 OptionName = :enumeration_issue_priorities
24 OptionName = :enumeration_issue_priorities
22
25
23 def option_name
26 def option_name
24 OptionName
27 OptionName
25 end
28 end
26
29
27 def objects_count
30 def objects_count
28 issues.count
31 issues.count
29 end
32 end
30
33
31 def transfer_relations(to)
34 def transfer_relations(to)
32 issues.update_all("priority_id = #{to.id}")
35 issues.update_all("priority_id = #{to.id}")
33 end
36 end
37
38 def css_classes
39 "priority-#{id} priority-#{position_name}"
40 end
41
42 # Clears position_name for all priorities
43 # Called from migration 20121026003537_populate_enumerations_position_name
44 def self.clear_position_names
45 update_all :position_name => nil
46 end
47
48 # Updates position_name for active priorities
49 # Called from migration 20121026003537_populate_enumerations_position_name
50 def self.compute_position_names
51 priorities = where(:active => true).all.sort_by(&:position)
52 if priorities.any?
53 default = priorities.detect(&:is_default?) || priorities[(priorities.size - 1) / 2]
54 priorities.each_with_index do |priority, index|
55 name = case
56 when priority.position == default.position
57 "default"
58 when priority.position < default.position
59 index == 0 ? "lowest" : "low#{index+1}"
60 else
61 index == (priorities.size - 1) ? "highest" : "high#{priorities.size - index}"
62 end
63
64 update_all({:position_name => name}, :id => priority.id)
65 end
66 end
67 end
34 end
68 end
@@ -1,98 +1,103
1 ---
1 ---
2 enumerations_001:
2 enumerations_001:
3 name: Uncategorized
3 name: Uncategorized
4 id: 1
4 id: 1
5 type: DocumentCategory
5 type: DocumentCategory
6 active: true
6 active: true
7 position: 1
7 position: 1
8 enumerations_002:
8 enumerations_002:
9 name: User documentation
9 name: User documentation
10 id: 2
10 id: 2
11 type: DocumentCategory
11 type: DocumentCategory
12 active: true
12 active: true
13 position: 2
13 position: 2
14 enumerations_003:
14 enumerations_003:
15 name: Technical documentation
15 name: Technical documentation
16 id: 3
16 id: 3
17 type: DocumentCategory
17 type: DocumentCategory
18 active: true
18 active: true
19 position: 3
19 position: 3
20 enumerations_004:
20 enumerations_004:
21 name: Low
21 name: Low
22 id: 4
22 id: 4
23 type: IssuePriority
23 type: IssuePriority
24 active: true
24 active: true
25 position: 1
25 position: 1
26 position_name: lowest
26 enumerations_005:
27 enumerations_005:
27 name: Normal
28 name: Normal
28 id: 5
29 id: 5
29 type: IssuePriority
30 type: IssuePriority
30 is_default: true
31 is_default: true
31 active: true
32 active: true
32 position: 2
33 position: 2
34 position_name: default
33 enumerations_006:
35 enumerations_006:
34 name: High
36 name: High
35 id: 6
37 id: 6
36 type: IssuePriority
38 type: IssuePriority
37 active: true
39 active: true
38 position: 3
40 position: 3
41 position_name: high3
39 enumerations_007:
42 enumerations_007:
40 name: Urgent
43 name: Urgent
41 id: 7
44 id: 7
42 type: IssuePriority
45 type: IssuePriority
43 active: true
46 active: true
44 position: 4
47 position: 4
48 position_name: high2
45 enumerations_008:
49 enumerations_008:
46 name: Immediate
50 name: Immediate
47 id: 8
51 id: 8
48 type: IssuePriority
52 type: IssuePriority
49 active: true
53 active: true
50 position: 5
54 position: 5
55 position_name: highest
51 enumerations_009:
56 enumerations_009:
52 name: Design
57 name: Design
53 id: 9
58 id: 9
54 type: TimeEntryActivity
59 type: TimeEntryActivity
55 position: 1
60 position: 1
56 active: true
61 active: true
57 enumerations_010:
62 enumerations_010:
58 name: Development
63 name: Development
59 id: 10
64 id: 10
60 type: TimeEntryActivity
65 type: TimeEntryActivity
61 position: 2
66 position: 2
62 is_default: true
67 is_default: true
63 active: true
68 active: true
64 enumerations_011:
69 enumerations_011:
65 name: QA
70 name: QA
66 id: 11
71 id: 11
67 type: TimeEntryActivity
72 type: TimeEntryActivity
68 position: 3
73 position: 3
69 active: true
74 active: true
70 enumerations_012:
75 enumerations_012:
71 name: Default Enumeration
76 name: Default Enumeration
72 id: 12
77 id: 12
73 type: Enumeration
78 type: Enumeration
74 is_default: true
79 is_default: true
75 active: true
80 active: true
76 enumerations_013:
81 enumerations_013:
77 name: Another Enumeration
82 name: Another Enumeration
78 id: 13
83 id: 13
79 type: Enumeration
84 type: Enumeration
80 active: true
85 active: true
81 enumerations_014:
86 enumerations_014:
82 name: Inactive Activity
87 name: Inactive Activity
83 id: 14
88 id: 14
84 type: TimeEntryActivity
89 type: TimeEntryActivity
85 position: 4
90 position: 4
86 active: false
91 active: false
87 enumerations_015:
92 enumerations_015:
88 name: Inactive Priority
93 name: Inactive Priority
89 id: 15
94 id: 15
90 type: IssuePriority
95 type: IssuePriority
91 position: 6
96 position: 6
92 active: false
97 active: false
93 enumerations_016:
98 enumerations_016:
94 name: Inactive Document Category
99 name: Inactive Document Category
95 id: 16
100 id: 16
96 type: DocumentCategory
101 type: DocumentCategory
97 active: false
102 active: false
98 position: 4
103 position: 4
@@ -1,76 +1,106
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 IssuePriorityTest < ActiveSupport::TestCase
20 class IssuePriorityTest < ActiveSupport::TestCase
21 fixtures :enumerations, :issues
21 fixtures :enumerations, :issues
22
22
23 def test_named_scope
23 def test_named_scope
24 assert_equal Enumeration.find_by_name('Normal'), Enumeration.named('normal').first
24 assert_equal Enumeration.find_by_name('Normal'), Enumeration.named('normal').first
25 end
25 end
26
26
27 def test_default_should_return_the_default_priority
27 def test_default_should_return_the_default_priority
28 assert_equal Enumeration.find_by_name('Normal'), IssuePriority.default
28 assert_equal Enumeration.find_by_name('Normal'), IssuePriority.default
29 end
29 end
30
30
31 def test_default_should_return_nil_when_no_default_priority
31 def test_default_should_return_nil_when_no_default_priority
32 IssuePriority.update_all :is_default => false
32 IssuePriority.update_all :is_default => false
33 assert_nil IssuePriority.default
33 assert_nil IssuePriority.default
34 end
34 end
35
35
36 def test_should_be_an_enumeration
36 def test_should_be_an_enumeration
37 assert IssuePriority.ancestors.include?(Enumeration)
37 assert IssuePriority.ancestors.include?(Enumeration)
38 end
38 end
39
39
40 def test_objects_count
40 def test_objects_count
41 # low priority
41 # low priority
42 assert_equal 6, IssuePriority.find(4).objects_count
42 assert_equal 6, IssuePriority.find(4).objects_count
43 # urgent
43 # urgent
44 assert_equal 0, IssuePriority.find(7).objects_count
44 assert_equal 0, IssuePriority.find(7).objects_count
45 end
45 end
46
46
47 def test_option_name
47 def test_option_name
48 assert_equal :enumeration_issue_priorities, IssuePriority.new.option_name
48 assert_equal :enumeration_issue_priorities, IssuePriority.new.option_name
49 end
49 end
50
50
51 def test_should_be_created_at_last_position
51 def test_should_be_created_at_last_position
52 IssuePriority.delete_all
52 IssuePriority.delete_all
53
53
54 priorities = [1, 2, 3].map {|i| IssuePriority.create!(:name => "P#{i}")}
54 priorities = [1, 2, 3].map {|i| IssuePriority.create!(:name => "P#{i}")}
55 assert_equal [1, 2, 3], priorities.map(&:position)
55 assert_equal [1, 2, 3], priorities.map(&:position)
56 end
56 end
57
57
58 def test_reset_positions_in_list_should_set_sequential_positions
58 def test_reset_positions_in_list_should_set_sequential_positions
59 IssuePriority.delete_all
59 IssuePriority.delete_all
60
60
61 priorities = [1, 2, 3].map {|i| IssuePriority.create!(:name => "P#{i}")}
61 priorities = [1, 2, 3].map {|i| IssuePriority.create!(:name => "P#{i}")}
62 priorities[0].update_attribute :position, 4
62 priorities[0].update_attribute :position, 4
63 priorities[1].update_attribute :position, 2
63 priorities[1].update_attribute :position, 2
64 priorities[2].update_attribute :position, 7
64 priorities[2].update_attribute :position, 7
65 assert_equal [4, 2, 7], priorities.map(&:reload).map(&:position)
65 assert_equal [4, 2, 7], priorities.map(&:reload).map(&:position)
66
66
67 priorities[0].reset_positions_in_list
67 priorities[0].reset_positions_in_list
68 assert_equal [2, 1, 3], priorities.map(&:reload).map(&:position)
68 assert_equal [2, 1, 3], priorities.map(&:reload).map(&:position)
69 end
69 end
70
70
71 def test_moving_in_list_should_reset_positions
71 def test_moving_in_list_should_reset_positions
72 priority = IssuePriority.first
72 priority = IssuePriority.first
73 priority.expects(:reset_positions_in_list).once
73 priority.expects(:reset_positions_in_list).once
74 priority.move_to = 'higher'
74 priority.move_to = 'higher'
75 end
75 end
76
77 def test_clear_position_names_should_set_position_names_to_nil
78 IssuePriority.clear_position_names
79 assert IssuePriority.all.all? {|priority| priority.position_name.nil?}
80 end
81
82 def test_compute_position_names_with_default_priority
83 IssuePriority.clear_position_names
84
85 IssuePriority.compute_position_names
86 assert_equal %w(lowest default high3 high2 highest), IssuePriority.active.all.sort.map(&:position_name)
87 end
88
89 def test_compute_position_names_without_default_priority_should_split_priorities
90 IssuePriority.clear_position_names
91 IssuePriority.update_all :is_default => false
92
93 IssuePriority.compute_position_names
94 assert_equal %w(lowest low2 default high2 highest), IssuePriority.active.all.sort.map(&:position_name)
95 end
96
97 def test_adding_a_priority_should_update_position_names
98 priority = IssuePriority.create!(:name => 'New')
99 assert_equal %w(lowest default high4 high3 high2 highest), IssuePriority.active.all.sort.map(&:position_name)
100 end
101
102 def test_destroying_a_priority_should_update_position_names
103 IssuePriority.find_by_position_name('highest').destroy
104 assert_equal %w(lowest default high2 highest), IssuePriority.active.all.sort.map(&:position_name)
105 end
76 end
106 end
@@ -1,1824 +1,1831
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, :journals, :journal_details,
28 :issues, :journals, :journal_details,
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 teardown
34 def teardown
35 User.current = nil
35 User.current = nil
36 end
36 end
37
37
38 def test_create
38 def test_create
39 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
39 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
40 :status_id => 1, :priority => IssuePriority.all.first,
40 :status_id => 1, :priority => IssuePriority.all.first,
41 :subject => 'test_create',
41 :subject => 'test_create',
42 :description => 'IssueTest#test_create', :estimated_hours => '1:30')
42 :description => 'IssueTest#test_create', :estimated_hours => '1:30')
43 assert issue.save
43 assert issue.save
44 issue.reload
44 issue.reload
45 assert_equal 1.5, issue.estimated_hours
45 assert_equal 1.5, issue.estimated_hours
46 end
46 end
47
47
48 def test_create_minimal
48 def test_create_minimal
49 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
49 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
50 :status_id => 1, :priority => IssuePriority.all.first,
50 :status_id => 1, :priority => IssuePriority.all.first,
51 :subject => 'test_create')
51 :subject => 'test_create')
52 assert issue.save
52 assert issue.save
53 assert issue.description.nil?
53 assert issue.description.nil?
54 assert_nil issue.estimated_hours
54 assert_nil issue.estimated_hours
55 end
55 end
56
56
57 def test_start_date_format_should_be_validated
57 def test_start_date_format_should_be_validated
58 set_language_if_valid 'en'
58 set_language_if_valid 'en'
59 ['2012', 'ABC', '2012-15-20'].each do |invalid_date|
59 ['2012', 'ABC', '2012-15-20'].each do |invalid_date|
60 issue = Issue.new(:start_date => invalid_date)
60 issue = Issue.new(:start_date => invalid_date)
61 assert !issue.valid?
61 assert !issue.valid?
62 assert_include 'Start date is not a valid date', issue.errors.full_messages, "No error found for invalid date #{invalid_date}"
62 assert_include 'Start date is not a valid date', issue.errors.full_messages, "No error found for invalid date #{invalid_date}"
63 end
63 end
64 end
64 end
65
65
66 def test_due_date_format_should_be_validated
66 def test_due_date_format_should_be_validated
67 set_language_if_valid 'en'
67 set_language_if_valid 'en'
68 ['2012', 'ABC', '2012-15-20'].each do |invalid_date|
68 ['2012', 'ABC', '2012-15-20'].each do |invalid_date|
69 issue = Issue.new(:due_date => invalid_date)
69 issue = Issue.new(:due_date => invalid_date)
70 assert !issue.valid?
70 assert !issue.valid?
71 assert_include 'Due date is not a valid date', issue.errors.full_messages, "No error found for invalid date #{invalid_date}"
71 assert_include 'Due date is not a valid date', issue.errors.full_messages, "No error found for invalid date #{invalid_date}"
72 end
72 end
73 end
73 end
74
74
75 def test_due_date_lesser_than_start_date_should_not_validate
75 def test_due_date_lesser_than_start_date_should_not_validate
76 set_language_if_valid 'en'
76 set_language_if_valid 'en'
77 issue = Issue.new(:start_date => '2012-10-06', :due_date => '2012-10-02')
77 issue = Issue.new(:start_date => '2012-10-06', :due_date => '2012-10-02')
78 assert !issue.valid?
78 assert !issue.valid?
79 assert_include 'Due date must be greater than start date', issue.errors.full_messages
79 assert_include 'Due date must be greater than start date', issue.errors.full_messages
80 end
80 end
81
81
82 def test_create_with_required_custom_field
82 def test_create_with_required_custom_field
83 set_language_if_valid 'en'
83 set_language_if_valid 'en'
84 field = IssueCustomField.find_by_name('Database')
84 field = IssueCustomField.find_by_name('Database')
85 field.update_attribute(:is_required, true)
85 field.update_attribute(:is_required, true)
86
86
87 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
87 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
88 :status_id => 1, :subject => 'test_create',
88 :status_id => 1, :subject => 'test_create',
89 :description => 'IssueTest#test_create_with_required_custom_field')
89 :description => 'IssueTest#test_create_with_required_custom_field')
90 assert issue.available_custom_fields.include?(field)
90 assert issue.available_custom_fields.include?(field)
91 # No value for the custom field
91 # No value for the custom field
92 assert !issue.save
92 assert !issue.save
93 assert_equal ["Database can't be blank"], issue.errors.full_messages
93 assert_equal ["Database can't be blank"], issue.errors.full_messages
94 # Blank value
94 # Blank value
95 issue.custom_field_values = { field.id => '' }
95 issue.custom_field_values = { field.id => '' }
96 assert !issue.save
96 assert !issue.save
97 assert_equal ["Database can't be blank"], issue.errors.full_messages
97 assert_equal ["Database can't be blank"], issue.errors.full_messages
98 # Invalid value
98 # Invalid value
99 issue.custom_field_values = { field.id => 'SQLServer' }
99 issue.custom_field_values = { field.id => 'SQLServer' }
100 assert !issue.save
100 assert !issue.save
101 assert_equal ["Database is not included in the list"], issue.errors.full_messages
101 assert_equal ["Database is not included in the list"], issue.errors.full_messages
102 # Valid value
102 # Valid value
103 issue.custom_field_values = { field.id => 'PostgreSQL' }
103 issue.custom_field_values = { field.id => 'PostgreSQL' }
104 assert issue.save
104 assert issue.save
105 issue.reload
105 issue.reload
106 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
106 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
107 end
107 end
108
108
109 def test_create_with_group_assignment
109 def test_create_with_group_assignment
110 with_settings :issue_group_assignment => '1' do
110 with_settings :issue_group_assignment => '1' do
111 assert Issue.new(:project_id => 2, :tracker_id => 1, :author_id => 1,
111 assert Issue.new(:project_id => 2, :tracker_id => 1, :author_id => 1,
112 :subject => 'Group assignment',
112 :subject => 'Group assignment',
113 :assigned_to_id => 11).save
113 :assigned_to_id => 11).save
114 issue = Issue.first(:order => 'id DESC')
114 issue = Issue.first(:order => 'id DESC')
115 assert_kind_of Group, issue.assigned_to
115 assert_kind_of Group, issue.assigned_to
116 assert_equal Group.find(11), issue.assigned_to
116 assert_equal Group.find(11), issue.assigned_to
117 end
117 end
118 end
118 end
119
119
120 def test_create_with_parent_issue_id
120 def test_create_with_parent_issue_id
121 issue = Issue.new(:project_id => 1, :tracker_id => 1,
121 issue = Issue.new(:project_id => 1, :tracker_id => 1,
122 :author_id => 1, :subject => 'Group assignment',
122 :author_id => 1, :subject => 'Group assignment',
123 :parent_issue_id => 1)
123 :parent_issue_id => 1)
124 assert_save issue
124 assert_save issue
125 assert_equal 1, issue.parent_issue_id
125 assert_equal 1, issue.parent_issue_id
126 assert_equal Issue.find(1), issue.parent
126 assert_equal Issue.find(1), issue.parent
127 end
127 end
128
128
129 def test_create_with_sharp_parent_issue_id
129 def test_create_with_sharp_parent_issue_id
130 issue = Issue.new(:project_id => 1, :tracker_id => 1,
130 issue = Issue.new(:project_id => 1, :tracker_id => 1,
131 :author_id => 1, :subject => 'Group assignment',
131 :author_id => 1, :subject => 'Group assignment',
132 :parent_issue_id => "#1")
132 :parent_issue_id => "#1")
133 assert_save issue
133 assert_save issue
134 assert_equal 1, issue.parent_issue_id
134 assert_equal 1, issue.parent_issue_id
135 assert_equal Issue.find(1), issue.parent
135 assert_equal Issue.find(1), issue.parent
136 end
136 end
137
137
138 def test_create_with_invalid_parent_issue_id
138 def test_create_with_invalid_parent_issue_id
139 set_language_if_valid 'en'
139 set_language_if_valid 'en'
140 issue = Issue.new(:project_id => 1, :tracker_id => 1,
140 issue = Issue.new(:project_id => 1, :tracker_id => 1,
141 :author_id => 1, :subject => 'Group assignment',
141 :author_id => 1, :subject => 'Group assignment',
142 :parent_issue_id => '01ABC')
142 :parent_issue_id => '01ABC')
143 assert !issue.save
143 assert !issue.save
144 assert_equal '01ABC', issue.parent_issue_id
144 assert_equal '01ABC', issue.parent_issue_id
145 assert_include 'Parent task is invalid', issue.errors.full_messages
145 assert_include 'Parent task is invalid', issue.errors.full_messages
146 end
146 end
147
147
148 def test_create_with_invalid_sharp_parent_issue_id
148 def test_create_with_invalid_sharp_parent_issue_id
149 set_language_if_valid 'en'
149 set_language_if_valid 'en'
150 issue = Issue.new(:project_id => 1, :tracker_id => 1,
150 issue = Issue.new(:project_id => 1, :tracker_id => 1,
151 :author_id => 1, :subject => 'Group assignment',
151 :author_id => 1, :subject => 'Group assignment',
152 :parent_issue_id => '#01ABC')
152 :parent_issue_id => '#01ABC')
153 assert !issue.save
153 assert !issue.save
154 assert_equal '#01ABC', issue.parent_issue_id
154 assert_equal '#01ABC', issue.parent_issue_id
155 assert_include 'Parent task is invalid', issue.errors.full_messages
155 assert_include 'Parent task is invalid', issue.errors.full_messages
156 end
156 end
157
157
158 def assert_visibility_match(user, issues)
158 def assert_visibility_match(user, issues)
159 assert_equal issues.collect(&:id).sort, Issue.all.select {|issue| issue.visible?(user)}.collect(&:id).sort
159 assert_equal issues.collect(&:id).sort, Issue.all.select {|issue| issue.visible?(user)}.collect(&:id).sort
160 end
160 end
161
161
162 def test_visible_scope_for_anonymous
162 def test_visible_scope_for_anonymous
163 # Anonymous user should see issues of public projects only
163 # Anonymous user should see issues of public projects only
164 issues = Issue.visible(User.anonymous).all
164 issues = Issue.visible(User.anonymous).all
165 assert issues.any?
165 assert issues.any?
166 assert_nil issues.detect {|issue| !issue.project.is_public?}
166 assert_nil issues.detect {|issue| !issue.project.is_public?}
167 assert_nil issues.detect {|issue| issue.is_private?}
167 assert_nil issues.detect {|issue| issue.is_private?}
168 assert_visibility_match User.anonymous, issues
168 assert_visibility_match User.anonymous, issues
169 end
169 end
170
170
171 def test_visible_scope_for_anonymous_without_view_issues_permissions
171 def test_visible_scope_for_anonymous_without_view_issues_permissions
172 # Anonymous user should not see issues without permission
172 # Anonymous user should not see issues without permission
173 Role.anonymous.remove_permission!(:view_issues)
173 Role.anonymous.remove_permission!(:view_issues)
174 issues = Issue.visible(User.anonymous).all
174 issues = Issue.visible(User.anonymous).all
175 assert issues.empty?
175 assert issues.empty?
176 assert_visibility_match User.anonymous, issues
176 assert_visibility_match User.anonymous, issues
177 end
177 end
178
178
179 def test_anonymous_should_not_see_private_issues_with_issues_visibility_set_to_default
179 def test_anonymous_should_not_see_private_issues_with_issues_visibility_set_to_default
180 assert Role.anonymous.update_attribute(:issues_visibility, 'default')
180 assert Role.anonymous.update_attribute(:issues_visibility, 'default')
181 issue = Issue.generate!(:author => User.anonymous, :assigned_to => User.anonymous, :is_private => true)
181 issue = Issue.generate!(:author => User.anonymous, :assigned_to => User.anonymous, :is_private => true)
182 assert_nil Issue.where(:id => issue.id).visible(User.anonymous).first
182 assert_nil Issue.where(:id => issue.id).visible(User.anonymous).first
183 assert !issue.visible?(User.anonymous)
183 assert !issue.visible?(User.anonymous)
184 end
184 end
185
185
186 def test_anonymous_should_not_see_private_issues_with_issues_visibility_set_to_own
186 def test_anonymous_should_not_see_private_issues_with_issues_visibility_set_to_own
187 assert Role.anonymous.update_attribute(:issues_visibility, 'own')
187 assert Role.anonymous.update_attribute(:issues_visibility, 'own')
188 issue = Issue.generate!(:author => User.anonymous, :assigned_to => User.anonymous, :is_private => true)
188 issue = Issue.generate!(:author => User.anonymous, :assigned_to => User.anonymous, :is_private => true)
189 assert_nil Issue.where(:id => issue.id).visible(User.anonymous).first
189 assert_nil Issue.where(:id => issue.id).visible(User.anonymous).first
190 assert !issue.visible?(User.anonymous)
190 assert !issue.visible?(User.anonymous)
191 end
191 end
192
192
193 def test_visible_scope_for_non_member
193 def test_visible_scope_for_non_member
194 user = User.find(9)
194 user = User.find(9)
195 assert user.projects.empty?
195 assert user.projects.empty?
196 # Non member user should see issues of public projects only
196 # Non member user should see issues of public projects only
197 issues = Issue.visible(user).all
197 issues = Issue.visible(user).all
198 assert issues.any?
198 assert issues.any?
199 assert_nil issues.detect {|issue| !issue.project.is_public?}
199 assert_nil issues.detect {|issue| !issue.project.is_public?}
200 assert_nil issues.detect {|issue| issue.is_private?}
200 assert_nil issues.detect {|issue| issue.is_private?}
201 assert_visibility_match user, issues
201 assert_visibility_match user, issues
202 end
202 end
203
203
204 def test_visible_scope_for_non_member_with_own_issues_visibility
204 def test_visible_scope_for_non_member_with_own_issues_visibility
205 Role.non_member.update_attribute :issues_visibility, 'own'
205 Role.non_member.update_attribute :issues_visibility, 'own'
206 Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 9, :subject => 'Issue by non member')
206 Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 9, :subject => 'Issue by non member')
207 user = User.find(9)
207 user = User.find(9)
208
208
209 issues = Issue.visible(user).all
209 issues = Issue.visible(user).all
210 assert issues.any?
210 assert issues.any?
211 assert_nil issues.detect {|issue| issue.author != user}
211 assert_nil issues.detect {|issue| issue.author != user}
212 assert_visibility_match user, issues
212 assert_visibility_match user, issues
213 end
213 end
214
214
215 def test_visible_scope_for_non_member_without_view_issues_permissions
215 def test_visible_scope_for_non_member_without_view_issues_permissions
216 # Non member user should not see issues without permission
216 # Non member user should not see issues without permission
217 Role.non_member.remove_permission!(:view_issues)
217 Role.non_member.remove_permission!(:view_issues)
218 user = User.find(9)
218 user = User.find(9)
219 assert user.projects.empty?
219 assert user.projects.empty?
220 issues = Issue.visible(user).all
220 issues = Issue.visible(user).all
221 assert issues.empty?
221 assert issues.empty?
222 assert_visibility_match user, issues
222 assert_visibility_match user, issues
223 end
223 end
224
224
225 def test_visible_scope_for_member
225 def test_visible_scope_for_member
226 user = User.find(9)
226 user = User.find(9)
227 # User should see issues of projects for which he has view_issues permissions only
227 # User should see issues of projects for which he has view_issues permissions only
228 Role.non_member.remove_permission!(:view_issues)
228 Role.non_member.remove_permission!(:view_issues)
229 Member.create!(:principal => user, :project_id => 3, :role_ids => [2])
229 Member.create!(:principal => user, :project_id => 3, :role_ids => [2])
230 issues = Issue.visible(user).all
230 issues = Issue.visible(user).all
231 assert issues.any?
231 assert issues.any?
232 assert_nil issues.detect {|issue| issue.project_id != 3}
232 assert_nil issues.detect {|issue| issue.project_id != 3}
233 assert_nil issues.detect {|issue| issue.is_private?}
233 assert_nil issues.detect {|issue| issue.is_private?}
234 assert_visibility_match user, issues
234 assert_visibility_match user, issues
235 end
235 end
236
236
237 def test_visible_scope_for_member_with_groups_should_return_assigned_issues
237 def test_visible_scope_for_member_with_groups_should_return_assigned_issues
238 user = User.find(8)
238 user = User.find(8)
239 assert user.groups.any?
239 assert user.groups.any?
240 Member.create!(:principal => user.groups.first, :project_id => 1, :role_ids => [2])
240 Member.create!(:principal => user.groups.first, :project_id => 1, :role_ids => [2])
241 Role.non_member.remove_permission!(:view_issues)
241 Role.non_member.remove_permission!(:view_issues)
242
242
243 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3,
243 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3,
244 :status_id => 1, :priority => IssuePriority.all.first,
244 :status_id => 1, :priority => IssuePriority.all.first,
245 :subject => 'Assignment test',
245 :subject => 'Assignment test',
246 :assigned_to => user.groups.first,
246 :assigned_to => user.groups.first,
247 :is_private => true)
247 :is_private => true)
248
248
249 Role.find(2).update_attribute :issues_visibility, 'default'
249 Role.find(2).update_attribute :issues_visibility, 'default'
250 issues = Issue.visible(User.find(8)).all
250 issues = Issue.visible(User.find(8)).all
251 assert issues.any?
251 assert issues.any?
252 assert issues.include?(issue)
252 assert issues.include?(issue)
253
253
254 Role.find(2).update_attribute :issues_visibility, 'own'
254 Role.find(2).update_attribute :issues_visibility, 'own'
255 issues = Issue.visible(User.find(8)).all
255 issues = Issue.visible(User.find(8)).all
256 assert issues.any?
256 assert issues.any?
257 assert issues.include?(issue)
257 assert issues.include?(issue)
258 end
258 end
259
259
260 def test_visible_scope_for_admin
260 def test_visible_scope_for_admin
261 user = User.find(1)
261 user = User.find(1)
262 user.members.each(&:destroy)
262 user.members.each(&:destroy)
263 assert user.projects.empty?
263 assert user.projects.empty?
264 issues = Issue.visible(user).all
264 issues = Issue.visible(user).all
265 assert issues.any?
265 assert issues.any?
266 # Admin should see issues on private projects that he does not belong to
266 # Admin should see issues on private projects that he does not belong to
267 assert issues.detect {|issue| !issue.project.is_public?}
267 assert issues.detect {|issue| !issue.project.is_public?}
268 # Admin should see private issues of other users
268 # Admin should see private issues of other users
269 assert issues.detect {|issue| issue.is_private? && issue.author != user}
269 assert issues.detect {|issue| issue.is_private? && issue.author != user}
270 assert_visibility_match user, issues
270 assert_visibility_match user, issues
271 end
271 end
272
272
273 def test_visible_scope_with_project
273 def test_visible_scope_with_project
274 project = Project.find(1)
274 project = Project.find(1)
275 issues = Issue.visible(User.find(2), :project => project).all
275 issues = Issue.visible(User.find(2), :project => project).all
276 projects = issues.collect(&:project).uniq
276 projects = issues.collect(&:project).uniq
277 assert_equal 1, projects.size
277 assert_equal 1, projects.size
278 assert_equal project, projects.first
278 assert_equal project, projects.first
279 end
279 end
280
280
281 def test_visible_scope_with_project_and_subprojects
281 def test_visible_scope_with_project_and_subprojects
282 project = Project.find(1)
282 project = Project.find(1)
283 issues = Issue.visible(User.find(2), :project => project, :with_subprojects => true).all
283 issues = Issue.visible(User.find(2), :project => project, :with_subprojects => true).all
284 projects = issues.collect(&:project).uniq
284 projects = issues.collect(&:project).uniq
285 assert projects.size > 1
285 assert projects.size > 1
286 assert_equal [], projects.select {|p| !p.is_or_is_descendant_of?(project)}
286 assert_equal [], projects.select {|p| !p.is_or_is_descendant_of?(project)}
287 end
287 end
288
288
289 def test_visible_and_nested_set_scopes
289 def test_visible_and_nested_set_scopes
290 assert_equal 0, Issue.find(1).descendants.visible.all.size
290 assert_equal 0, Issue.find(1).descendants.visible.all.size
291 end
291 end
292
292
293 def test_open_scope
293 def test_open_scope
294 issues = Issue.open.all
294 issues = Issue.open.all
295 assert_nil issues.detect(&:closed?)
295 assert_nil issues.detect(&:closed?)
296 end
296 end
297
297
298 def test_open_scope_with_arg
298 def test_open_scope_with_arg
299 issues = Issue.open(false).all
299 issues = Issue.open(false).all
300 assert_equal issues, issues.select(&:closed?)
300 assert_equal issues, issues.select(&:closed?)
301 end
301 end
302
302
303 def test_errors_full_messages_should_include_custom_fields_errors
303 def test_errors_full_messages_should_include_custom_fields_errors
304 field = IssueCustomField.find_by_name('Database')
304 field = IssueCustomField.find_by_name('Database')
305
305
306 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
306 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
307 :status_id => 1, :subject => 'test_create',
307 :status_id => 1, :subject => 'test_create',
308 :description => 'IssueTest#test_create_with_required_custom_field')
308 :description => 'IssueTest#test_create_with_required_custom_field')
309 assert issue.available_custom_fields.include?(field)
309 assert issue.available_custom_fields.include?(field)
310 # Invalid value
310 # Invalid value
311 issue.custom_field_values = { field.id => 'SQLServer' }
311 issue.custom_field_values = { field.id => 'SQLServer' }
312
312
313 assert !issue.valid?
313 assert !issue.valid?
314 assert_equal 1, issue.errors.full_messages.size
314 assert_equal 1, issue.errors.full_messages.size
315 assert_equal "Database #{I18n.translate('activerecord.errors.messages.inclusion')}",
315 assert_equal "Database #{I18n.translate('activerecord.errors.messages.inclusion')}",
316 issue.errors.full_messages.first
316 issue.errors.full_messages.first
317 end
317 end
318
318
319 def test_update_issue_with_required_custom_field
319 def test_update_issue_with_required_custom_field
320 field = IssueCustomField.find_by_name('Database')
320 field = IssueCustomField.find_by_name('Database')
321 field.update_attribute(:is_required, true)
321 field.update_attribute(:is_required, true)
322
322
323 issue = Issue.find(1)
323 issue = Issue.find(1)
324 assert_nil issue.custom_value_for(field)
324 assert_nil issue.custom_value_for(field)
325 assert issue.available_custom_fields.include?(field)
325 assert issue.available_custom_fields.include?(field)
326 # No change to custom values, issue can be saved
326 # No change to custom values, issue can be saved
327 assert issue.save
327 assert issue.save
328 # Blank value
328 # Blank value
329 issue.custom_field_values = { field.id => '' }
329 issue.custom_field_values = { field.id => '' }
330 assert !issue.save
330 assert !issue.save
331 # Valid value
331 # Valid value
332 issue.custom_field_values = { field.id => 'PostgreSQL' }
332 issue.custom_field_values = { field.id => 'PostgreSQL' }
333 assert issue.save
333 assert issue.save
334 issue.reload
334 issue.reload
335 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
335 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
336 end
336 end
337
337
338 def test_should_not_update_attributes_if_custom_fields_validation_fails
338 def test_should_not_update_attributes_if_custom_fields_validation_fails
339 issue = Issue.find(1)
339 issue = Issue.find(1)
340 field = IssueCustomField.find_by_name('Database')
340 field = IssueCustomField.find_by_name('Database')
341 assert issue.available_custom_fields.include?(field)
341 assert issue.available_custom_fields.include?(field)
342
342
343 issue.custom_field_values = { field.id => 'Invalid' }
343 issue.custom_field_values = { field.id => 'Invalid' }
344 issue.subject = 'Should be not be saved'
344 issue.subject = 'Should be not be saved'
345 assert !issue.save
345 assert !issue.save
346
346
347 issue.reload
347 issue.reload
348 assert_equal "Can't print recipes", issue.subject
348 assert_equal "Can't print recipes", issue.subject
349 end
349 end
350
350
351 def test_should_not_recreate_custom_values_objects_on_update
351 def test_should_not_recreate_custom_values_objects_on_update
352 field = IssueCustomField.find_by_name('Database')
352 field = IssueCustomField.find_by_name('Database')
353
353
354 issue = Issue.find(1)
354 issue = Issue.find(1)
355 issue.custom_field_values = { field.id => 'PostgreSQL' }
355 issue.custom_field_values = { field.id => 'PostgreSQL' }
356 assert issue.save
356 assert issue.save
357 custom_value = issue.custom_value_for(field)
357 custom_value = issue.custom_value_for(field)
358 issue.reload
358 issue.reload
359 issue.custom_field_values = { field.id => 'MySQL' }
359 issue.custom_field_values = { field.id => 'MySQL' }
360 assert issue.save
360 assert issue.save
361 issue.reload
361 issue.reload
362 assert_equal custom_value.id, issue.custom_value_for(field).id
362 assert_equal custom_value.id, issue.custom_value_for(field).id
363 end
363 end
364
364
365 def test_should_not_update_custom_fields_on_changing_tracker_with_different_custom_fields
365 def test_should_not_update_custom_fields_on_changing_tracker_with_different_custom_fields
366 issue = Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1,
366 issue = Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1,
367 :status_id => 1, :subject => 'Test',
367 :status_id => 1, :subject => 'Test',
368 :custom_field_values => {'2' => 'Test'})
368 :custom_field_values => {'2' => 'Test'})
369 assert !Tracker.find(2).custom_field_ids.include?(2)
369 assert !Tracker.find(2).custom_field_ids.include?(2)
370
370
371 issue = Issue.find(issue.id)
371 issue = Issue.find(issue.id)
372 issue.attributes = {:tracker_id => 2, :custom_field_values => {'1' => ''}}
372 issue.attributes = {:tracker_id => 2, :custom_field_values => {'1' => ''}}
373
373
374 issue = Issue.find(issue.id)
374 issue = Issue.find(issue.id)
375 custom_value = issue.custom_value_for(2)
375 custom_value = issue.custom_value_for(2)
376 assert_not_nil custom_value
376 assert_not_nil custom_value
377 assert_equal 'Test', custom_value.value
377 assert_equal 'Test', custom_value.value
378 end
378 end
379
379
380 def test_assigning_tracker_id_should_reload_custom_fields_values
380 def test_assigning_tracker_id_should_reload_custom_fields_values
381 issue = Issue.new(:project => Project.find(1))
381 issue = Issue.new(:project => Project.find(1))
382 assert issue.custom_field_values.empty?
382 assert issue.custom_field_values.empty?
383 issue.tracker_id = 1
383 issue.tracker_id = 1
384 assert issue.custom_field_values.any?
384 assert issue.custom_field_values.any?
385 end
385 end
386
386
387 def test_assigning_attributes_should_assign_project_and_tracker_first
387 def test_assigning_attributes_should_assign_project_and_tracker_first
388 seq = sequence('seq')
388 seq = sequence('seq')
389 issue = Issue.new
389 issue = Issue.new
390 issue.expects(:project_id=).in_sequence(seq)
390 issue.expects(:project_id=).in_sequence(seq)
391 issue.expects(:tracker_id=).in_sequence(seq)
391 issue.expects(:tracker_id=).in_sequence(seq)
392 issue.expects(:subject=).in_sequence(seq)
392 issue.expects(:subject=).in_sequence(seq)
393 issue.attributes = {:tracker_id => 2, :project_id => 1, :subject => 'Test'}
393 issue.attributes = {:tracker_id => 2, :project_id => 1, :subject => 'Test'}
394 end
394 end
395
395
396 def test_assigning_tracker_and_custom_fields_should_assign_custom_fields
396 def test_assigning_tracker_and_custom_fields_should_assign_custom_fields
397 attributes = ActiveSupport::OrderedHash.new
397 attributes = ActiveSupport::OrderedHash.new
398 attributes['custom_field_values'] = { '1' => 'MySQL' }
398 attributes['custom_field_values'] = { '1' => 'MySQL' }
399 attributes['tracker_id'] = '1'
399 attributes['tracker_id'] = '1'
400 issue = Issue.new(:project => Project.find(1))
400 issue = Issue.new(:project => Project.find(1))
401 issue.attributes = attributes
401 issue.attributes = attributes
402 assert_equal 'MySQL', issue.custom_field_value(1)
402 assert_equal 'MySQL', issue.custom_field_value(1)
403 end
403 end
404
404
405 def test_should_update_issue_with_disabled_tracker
405 def test_should_update_issue_with_disabled_tracker
406 p = Project.find(1)
406 p = Project.find(1)
407 issue = Issue.find(1)
407 issue = Issue.find(1)
408
408
409 p.trackers.delete(issue.tracker)
409 p.trackers.delete(issue.tracker)
410 assert !p.trackers.include?(issue.tracker)
410 assert !p.trackers.include?(issue.tracker)
411
411
412 issue.reload
412 issue.reload
413 issue.subject = 'New subject'
413 issue.subject = 'New subject'
414 assert issue.save
414 assert issue.save
415 end
415 end
416
416
417 def test_should_not_set_a_disabled_tracker
417 def test_should_not_set_a_disabled_tracker
418 p = Project.find(1)
418 p = Project.find(1)
419 p.trackers.delete(Tracker.find(2))
419 p.trackers.delete(Tracker.find(2))
420
420
421 issue = Issue.find(1)
421 issue = Issue.find(1)
422 issue.tracker_id = 2
422 issue.tracker_id = 2
423 issue.subject = 'New subject'
423 issue.subject = 'New subject'
424 assert !issue.save
424 assert !issue.save
425 assert_not_nil issue.errors[:tracker_id]
425 assert_not_nil issue.errors[:tracker_id]
426 end
426 end
427
427
428 def test_category_based_assignment
428 def test_category_based_assignment
429 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3,
429 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3,
430 :status_id => 1, :priority => IssuePriority.all.first,
430 :status_id => 1, :priority => IssuePriority.all.first,
431 :subject => 'Assignment test',
431 :subject => 'Assignment test',
432 :description => 'Assignment test', :category_id => 1)
432 :description => 'Assignment test', :category_id => 1)
433 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
433 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
434 end
434 end
435
435
436 def test_new_statuses_allowed_to
436 def test_new_statuses_allowed_to
437 WorkflowTransition.delete_all
437 WorkflowTransition.delete_all
438 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
438 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
439 :old_status_id => 1, :new_status_id => 2,
439 :old_status_id => 1, :new_status_id => 2,
440 :author => false, :assignee => false)
440 :author => false, :assignee => false)
441 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
441 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
442 :old_status_id => 1, :new_status_id => 3,
442 :old_status_id => 1, :new_status_id => 3,
443 :author => true, :assignee => false)
443 :author => true, :assignee => false)
444 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1,
444 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1,
445 :new_status_id => 4, :author => false,
445 :new_status_id => 4, :author => false,
446 :assignee => true)
446 :assignee => true)
447 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
447 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
448 :old_status_id => 1, :new_status_id => 5,
448 :old_status_id => 1, :new_status_id => 5,
449 :author => true, :assignee => true)
449 :author => true, :assignee => true)
450 status = IssueStatus.find(1)
450 status = IssueStatus.find(1)
451 role = Role.find(1)
451 role = Role.find(1)
452 tracker = Tracker.find(1)
452 tracker = Tracker.find(1)
453 user = User.find(2)
453 user = User.find(2)
454
454
455 issue = Issue.generate!(:tracker => tracker, :status => status,
455 issue = Issue.generate!(:tracker => tracker, :status => status,
456 :project_id => 1, :author_id => 1)
456 :project_id => 1, :author_id => 1)
457 assert_equal [1, 2], issue.new_statuses_allowed_to(user).map(&:id)
457 assert_equal [1, 2], issue.new_statuses_allowed_to(user).map(&:id)
458
458
459 issue = Issue.generate!(:tracker => tracker, :status => status,
459 issue = Issue.generate!(:tracker => tracker, :status => status,
460 :project_id => 1, :author => user)
460 :project_id => 1, :author => user)
461 assert_equal [1, 2, 3, 5], issue.new_statuses_allowed_to(user).map(&:id)
461 assert_equal [1, 2, 3, 5], issue.new_statuses_allowed_to(user).map(&:id)
462
462
463 issue = Issue.generate!(:tracker => tracker, :status => status,
463 issue = Issue.generate!(:tracker => tracker, :status => status,
464 :project_id => 1, :author_id => 1,
464 :project_id => 1, :author_id => 1,
465 :assigned_to => user)
465 :assigned_to => user)
466 assert_equal [1, 2, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
466 assert_equal [1, 2, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
467
467
468 issue = Issue.generate!(:tracker => tracker, :status => status,
468 issue = Issue.generate!(:tracker => tracker, :status => status,
469 :project_id => 1, :author => user,
469 :project_id => 1, :author => user,
470 :assigned_to => user)
470 :assigned_to => user)
471 assert_equal [1, 2, 3, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
471 assert_equal [1, 2, 3, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
472 end
472 end
473
473
474 def test_new_statuses_allowed_to_should_return_all_transitions_for_admin
474 def test_new_statuses_allowed_to_should_return_all_transitions_for_admin
475 admin = User.find(1)
475 admin = User.find(1)
476 issue = Issue.find(1)
476 issue = Issue.find(1)
477 assert !admin.member_of?(issue.project)
477 assert !admin.member_of?(issue.project)
478 expected_statuses = [issue.status] +
478 expected_statuses = [issue.status] +
479 WorkflowTransition.find_all_by_old_status_id(
479 WorkflowTransition.find_all_by_old_status_id(
480 issue.status_id).map(&:new_status).uniq.sort
480 issue.status_id).map(&:new_status).uniq.sort
481 assert_equal expected_statuses, issue.new_statuses_allowed_to(admin)
481 assert_equal expected_statuses, issue.new_statuses_allowed_to(admin)
482 end
482 end
483
483
484 def test_new_statuses_allowed_to_should_return_default_and_current_status_when_copying
484 def test_new_statuses_allowed_to_should_return_default_and_current_status_when_copying
485 issue = Issue.find(1).copy
485 issue = Issue.find(1).copy
486 assert_equal [1], issue.new_statuses_allowed_to(User.find(2)).map(&:id)
486 assert_equal [1], issue.new_statuses_allowed_to(User.find(2)).map(&:id)
487
487
488 issue = Issue.find(2).copy
488 issue = Issue.find(2).copy
489 assert_equal [1, 2], issue.new_statuses_allowed_to(User.find(2)).map(&:id)
489 assert_equal [1, 2], issue.new_statuses_allowed_to(User.find(2)).map(&:id)
490 end
490 end
491
491
492 def test_safe_attributes_names_should_not_include_disabled_field
492 def test_safe_attributes_names_should_not_include_disabled_field
493 tracker = Tracker.new(:core_fields => %w(assigned_to_id fixed_version_id))
493 tracker = Tracker.new(:core_fields => %w(assigned_to_id fixed_version_id))
494
494
495 issue = Issue.new(:tracker => tracker)
495 issue = Issue.new(:tracker => tracker)
496 assert_include 'tracker_id', issue.safe_attribute_names
496 assert_include 'tracker_id', issue.safe_attribute_names
497 assert_include 'status_id', issue.safe_attribute_names
497 assert_include 'status_id', issue.safe_attribute_names
498 assert_include 'subject', issue.safe_attribute_names
498 assert_include 'subject', issue.safe_attribute_names
499 assert_include 'description', issue.safe_attribute_names
499 assert_include 'description', issue.safe_attribute_names
500 assert_include 'custom_field_values', issue.safe_attribute_names
500 assert_include 'custom_field_values', issue.safe_attribute_names
501 assert_include 'custom_fields', issue.safe_attribute_names
501 assert_include 'custom_fields', issue.safe_attribute_names
502 assert_include 'lock_version', issue.safe_attribute_names
502 assert_include 'lock_version', issue.safe_attribute_names
503
503
504 tracker.core_fields.each do |field|
504 tracker.core_fields.each do |field|
505 assert_include field, issue.safe_attribute_names
505 assert_include field, issue.safe_attribute_names
506 end
506 end
507
507
508 tracker.disabled_core_fields.each do |field|
508 tracker.disabled_core_fields.each do |field|
509 assert_not_include field, issue.safe_attribute_names
509 assert_not_include field, issue.safe_attribute_names
510 end
510 end
511 end
511 end
512
512
513 def test_safe_attributes_should_ignore_disabled_fields
513 def test_safe_attributes_should_ignore_disabled_fields
514 tracker = Tracker.find(1)
514 tracker = Tracker.find(1)
515 tracker.core_fields = %w(assigned_to_id due_date)
515 tracker.core_fields = %w(assigned_to_id due_date)
516 tracker.save!
516 tracker.save!
517
517
518 issue = Issue.new(:tracker => tracker)
518 issue = Issue.new(:tracker => tracker)
519 issue.safe_attributes = {'start_date' => '2012-07-14', 'due_date' => '2012-07-14'}
519 issue.safe_attributes = {'start_date' => '2012-07-14', 'due_date' => '2012-07-14'}
520 assert_nil issue.start_date
520 assert_nil issue.start_date
521 assert_equal Date.parse('2012-07-14'), issue.due_date
521 assert_equal Date.parse('2012-07-14'), issue.due_date
522 end
522 end
523
523
524 def test_safe_attributes_should_accept_target_tracker_enabled_fields
524 def test_safe_attributes_should_accept_target_tracker_enabled_fields
525 source = Tracker.find(1)
525 source = Tracker.find(1)
526 source.core_fields = []
526 source.core_fields = []
527 source.save!
527 source.save!
528 target = Tracker.find(2)
528 target = Tracker.find(2)
529 target.core_fields = %w(assigned_to_id due_date)
529 target.core_fields = %w(assigned_to_id due_date)
530 target.save!
530 target.save!
531
531
532 issue = Issue.new(:tracker => source)
532 issue = Issue.new(:tracker => source)
533 issue.safe_attributes = {'tracker_id' => 2, 'due_date' => '2012-07-14'}
533 issue.safe_attributes = {'tracker_id' => 2, 'due_date' => '2012-07-14'}
534 assert_equal target, issue.tracker
534 assert_equal target, issue.tracker
535 assert_equal Date.parse('2012-07-14'), issue.due_date
535 assert_equal Date.parse('2012-07-14'), issue.due_date
536 end
536 end
537
537
538 def test_safe_attributes_should_not_include_readonly_fields
538 def test_safe_attributes_should_not_include_readonly_fields
539 WorkflowPermission.delete_all
539 WorkflowPermission.delete_all
540 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
540 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
541 :role_id => 1, :field_name => 'due_date',
541 :role_id => 1, :field_name => 'due_date',
542 :rule => 'readonly')
542 :rule => 'readonly')
543 user = User.find(2)
543 user = User.find(2)
544
544
545 issue = Issue.new(:project_id => 1, :tracker_id => 1)
545 issue = Issue.new(:project_id => 1, :tracker_id => 1)
546 assert_equal %w(due_date), issue.read_only_attribute_names(user)
546 assert_equal %w(due_date), issue.read_only_attribute_names(user)
547 assert_not_include 'due_date', issue.safe_attribute_names(user)
547 assert_not_include 'due_date', issue.safe_attribute_names(user)
548
548
549 issue.send :safe_attributes=, {'start_date' => '2012-07-14', 'due_date' => '2012-07-14'}, user
549 issue.send :safe_attributes=, {'start_date' => '2012-07-14', 'due_date' => '2012-07-14'}, user
550 assert_equal Date.parse('2012-07-14'), issue.start_date
550 assert_equal Date.parse('2012-07-14'), issue.start_date
551 assert_nil issue.due_date
551 assert_nil issue.due_date
552 end
552 end
553
553
554 def test_safe_attributes_should_not_include_readonly_custom_fields
554 def test_safe_attributes_should_not_include_readonly_custom_fields
555 cf1 = IssueCustomField.create!(:name => 'Writable field',
555 cf1 = IssueCustomField.create!(:name => 'Writable field',
556 :field_format => 'string',
556 :field_format => 'string',
557 :is_for_all => true, :tracker_ids => [1])
557 :is_for_all => true, :tracker_ids => [1])
558 cf2 = IssueCustomField.create!(:name => 'Readonly field',
558 cf2 = IssueCustomField.create!(:name => 'Readonly field',
559 :field_format => 'string',
559 :field_format => 'string',
560 :is_for_all => true, :tracker_ids => [1])
560 :is_for_all => true, :tracker_ids => [1])
561 WorkflowPermission.delete_all
561 WorkflowPermission.delete_all
562 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
562 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
563 :role_id => 1, :field_name => cf2.id.to_s,
563 :role_id => 1, :field_name => cf2.id.to_s,
564 :rule => 'readonly')
564 :rule => 'readonly')
565 user = User.find(2)
565 user = User.find(2)
566 issue = Issue.new(:project_id => 1, :tracker_id => 1)
566 issue = Issue.new(:project_id => 1, :tracker_id => 1)
567 assert_equal [cf2.id.to_s], issue.read_only_attribute_names(user)
567 assert_equal [cf2.id.to_s], issue.read_only_attribute_names(user)
568 assert_not_include cf2.id.to_s, issue.safe_attribute_names(user)
568 assert_not_include cf2.id.to_s, issue.safe_attribute_names(user)
569
569
570 issue.send :safe_attributes=, {'custom_field_values' => {
570 issue.send :safe_attributes=, {'custom_field_values' => {
571 cf1.id.to_s => 'value1', cf2.id.to_s => 'value2'
571 cf1.id.to_s => 'value1', cf2.id.to_s => 'value2'
572 }}, user
572 }}, user
573 assert_equal 'value1', issue.custom_field_value(cf1)
573 assert_equal 'value1', issue.custom_field_value(cf1)
574 assert_nil issue.custom_field_value(cf2)
574 assert_nil issue.custom_field_value(cf2)
575
575
576 issue.send :safe_attributes=, {'custom_fields' => [
576 issue.send :safe_attributes=, {'custom_fields' => [
577 {'id' => cf1.id.to_s, 'value' => 'valuea'},
577 {'id' => cf1.id.to_s, 'value' => 'valuea'},
578 {'id' => cf2.id.to_s, 'value' => 'valueb'}
578 {'id' => cf2.id.to_s, 'value' => 'valueb'}
579 ]}, user
579 ]}, user
580 assert_equal 'valuea', issue.custom_field_value(cf1)
580 assert_equal 'valuea', issue.custom_field_value(cf1)
581 assert_nil issue.custom_field_value(cf2)
581 assert_nil issue.custom_field_value(cf2)
582 end
582 end
583
583
584 def test_editable_custom_field_values_should_return_non_readonly_custom_values
584 def test_editable_custom_field_values_should_return_non_readonly_custom_values
585 cf1 = IssueCustomField.create!(:name => 'Writable field', :field_format => 'string',
585 cf1 = IssueCustomField.create!(:name => 'Writable field', :field_format => 'string',
586 :is_for_all => true, :tracker_ids => [1, 2])
586 :is_for_all => true, :tracker_ids => [1, 2])
587 cf2 = IssueCustomField.create!(:name => 'Readonly field', :field_format => 'string',
587 cf2 = IssueCustomField.create!(:name => 'Readonly field', :field_format => 'string',
588 :is_for_all => true, :tracker_ids => [1, 2])
588 :is_for_all => true, :tracker_ids => [1, 2])
589 WorkflowPermission.delete_all
589 WorkflowPermission.delete_all
590 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1,
590 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1,
591 :field_name => cf2.id.to_s, :rule => 'readonly')
591 :field_name => cf2.id.to_s, :rule => 'readonly')
592 user = User.find(2)
592 user = User.find(2)
593
593
594 issue = Issue.new(:project_id => 1, :tracker_id => 1)
594 issue = Issue.new(:project_id => 1, :tracker_id => 1)
595 values = issue.editable_custom_field_values(user)
595 values = issue.editable_custom_field_values(user)
596 assert values.detect {|value| value.custom_field == cf1}
596 assert values.detect {|value| value.custom_field == cf1}
597 assert_nil values.detect {|value| value.custom_field == cf2}
597 assert_nil values.detect {|value| value.custom_field == cf2}
598
598
599 issue.tracker_id = 2
599 issue.tracker_id = 2
600 values = issue.editable_custom_field_values(user)
600 values = issue.editable_custom_field_values(user)
601 assert values.detect {|value| value.custom_field == cf1}
601 assert values.detect {|value| value.custom_field == cf1}
602 assert values.detect {|value| value.custom_field == cf2}
602 assert values.detect {|value| value.custom_field == cf2}
603 end
603 end
604
604
605 def test_safe_attributes_should_accept_target_tracker_writable_fields
605 def test_safe_attributes_should_accept_target_tracker_writable_fields
606 WorkflowPermission.delete_all
606 WorkflowPermission.delete_all
607 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
607 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
608 :role_id => 1, :field_name => 'due_date',
608 :role_id => 1, :field_name => 'due_date',
609 :rule => 'readonly')
609 :rule => 'readonly')
610 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2,
610 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2,
611 :role_id => 1, :field_name => 'start_date',
611 :role_id => 1, :field_name => 'start_date',
612 :rule => 'readonly')
612 :rule => 'readonly')
613 user = User.find(2)
613 user = User.find(2)
614
614
615 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
615 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
616
616
617 issue.send :safe_attributes=, {'start_date' => '2012-07-12',
617 issue.send :safe_attributes=, {'start_date' => '2012-07-12',
618 'due_date' => '2012-07-14'}, user
618 'due_date' => '2012-07-14'}, user
619 assert_equal Date.parse('2012-07-12'), issue.start_date
619 assert_equal Date.parse('2012-07-12'), issue.start_date
620 assert_nil issue.due_date
620 assert_nil issue.due_date
621
621
622 issue.send :safe_attributes=, {'start_date' => '2012-07-15',
622 issue.send :safe_attributes=, {'start_date' => '2012-07-15',
623 'due_date' => '2012-07-16',
623 'due_date' => '2012-07-16',
624 'tracker_id' => 2}, user
624 'tracker_id' => 2}, user
625 assert_equal Date.parse('2012-07-12'), issue.start_date
625 assert_equal Date.parse('2012-07-12'), issue.start_date
626 assert_equal Date.parse('2012-07-16'), issue.due_date
626 assert_equal Date.parse('2012-07-16'), issue.due_date
627 end
627 end
628
628
629 def test_safe_attributes_should_accept_target_status_writable_fields
629 def test_safe_attributes_should_accept_target_status_writable_fields
630 WorkflowPermission.delete_all
630 WorkflowPermission.delete_all
631 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
631 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
632 :role_id => 1, :field_name => 'due_date',
632 :role_id => 1, :field_name => 'due_date',
633 :rule => 'readonly')
633 :rule => 'readonly')
634 WorkflowPermission.create!(:old_status_id => 2, :tracker_id => 1,
634 WorkflowPermission.create!(:old_status_id => 2, :tracker_id => 1,
635 :role_id => 1, :field_name => 'start_date',
635 :role_id => 1, :field_name => 'start_date',
636 :rule => 'readonly')
636 :rule => 'readonly')
637 user = User.find(2)
637 user = User.find(2)
638
638
639 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
639 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
640
640
641 issue.send :safe_attributes=, {'start_date' => '2012-07-12',
641 issue.send :safe_attributes=, {'start_date' => '2012-07-12',
642 'due_date' => '2012-07-14'},
642 'due_date' => '2012-07-14'},
643 user
643 user
644 assert_equal Date.parse('2012-07-12'), issue.start_date
644 assert_equal Date.parse('2012-07-12'), issue.start_date
645 assert_nil issue.due_date
645 assert_nil issue.due_date
646
646
647 issue.send :safe_attributes=, {'start_date' => '2012-07-15',
647 issue.send :safe_attributes=, {'start_date' => '2012-07-15',
648 'due_date' => '2012-07-16',
648 'due_date' => '2012-07-16',
649 'status_id' => 2},
649 'status_id' => 2},
650 user
650 user
651 assert_equal Date.parse('2012-07-12'), issue.start_date
651 assert_equal Date.parse('2012-07-12'), issue.start_date
652 assert_equal Date.parse('2012-07-16'), issue.due_date
652 assert_equal Date.parse('2012-07-16'), issue.due_date
653 end
653 end
654
654
655 def test_required_attributes_should_be_validated
655 def test_required_attributes_should_be_validated
656 cf = IssueCustomField.create!(:name => 'Foo', :field_format => 'string',
656 cf = IssueCustomField.create!(:name => 'Foo', :field_format => 'string',
657 :is_for_all => true, :tracker_ids => [1, 2])
657 :is_for_all => true, :tracker_ids => [1, 2])
658
658
659 WorkflowPermission.delete_all
659 WorkflowPermission.delete_all
660 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
660 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
661 :role_id => 1, :field_name => 'due_date',
661 :role_id => 1, :field_name => 'due_date',
662 :rule => 'required')
662 :rule => 'required')
663 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
663 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
664 :role_id => 1, :field_name => 'category_id',
664 :role_id => 1, :field_name => 'category_id',
665 :rule => 'required')
665 :rule => 'required')
666 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
666 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
667 :role_id => 1, :field_name => cf.id.to_s,
667 :role_id => 1, :field_name => cf.id.to_s,
668 :rule => 'required')
668 :rule => 'required')
669
669
670 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2,
670 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2,
671 :role_id => 1, :field_name => 'start_date',
671 :role_id => 1, :field_name => 'start_date',
672 :rule => 'required')
672 :rule => 'required')
673 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2,
673 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2,
674 :role_id => 1, :field_name => cf.id.to_s,
674 :role_id => 1, :field_name => cf.id.to_s,
675 :rule => 'required')
675 :rule => 'required')
676 user = User.find(2)
676 user = User.find(2)
677
677
678 issue = Issue.new(:project_id => 1, :tracker_id => 1,
678 issue = Issue.new(:project_id => 1, :tracker_id => 1,
679 :status_id => 1, :subject => 'Required fields',
679 :status_id => 1, :subject => 'Required fields',
680 :author => user)
680 :author => user)
681 assert_equal [cf.id.to_s, "category_id", "due_date"],
681 assert_equal [cf.id.to_s, "category_id", "due_date"],
682 issue.required_attribute_names(user).sort
682 issue.required_attribute_names(user).sort
683 assert !issue.save, "Issue was saved"
683 assert !issue.save, "Issue was saved"
684 assert_equal ["Category can't be blank", "Due date can't be blank", "Foo can't be blank"],
684 assert_equal ["Category can't be blank", "Due date can't be blank", "Foo can't be blank"],
685 issue.errors.full_messages.sort
685 issue.errors.full_messages.sort
686
686
687 issue.tracker_id = 2
687 issue.tracker_id = 2
688 assert_equal [cf.id.to_s, "start_date"], issue.required_attribute_names(user).sort
688 assert_equal [cf.id.to_s, "start_date"], issue.required_attribute_names(user).sort
689 assert !issue.save, "Issue was saved"
689 assert !issue.save, "Issue was saved"
690 assert_equal ["Foo can't be blank", "Start date can't be blank"],
690 assert_equal ["Foo can't be blank", "Start date can't be blank"],
691 issue.errors.full_messages.sort
691 issue.errors.full_messages.sort
692
692
693 issue.start_date = Date.today
693 issue.start_date = Date.today
694 issue.custom_field_values = {cf.id.to_s => 'bar'}
694 issue.custom_field_values = {cf.id.to_s => 'bar'}
695 assert issue.save
695 assert issue.save
696 end
696 end
697
697
698 def test_required_attribute_names_for_multiple_roles_should_intersect_rules
698 def test_required_attribute_names_for_multiple_roles_should_intersect_rules
699 WorkflowPermission.delete_all
699 WorkflowPermission.delete_all
700 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
700 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
701 :role_id => 1, :field_name => 'due_date',
701 :role_id => 1, :field_name => 'due_date',
702 :rule => 'required')
702 :rule => 'required')
703 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
703 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
704 :role_id => 1, :field_name => 'start_date',
704 :role_id => 1, :field_name => 'start_date',
705 :rule => 'required')
705 :rule => 'required')
706 user = User.find(2)
706 user = User.find(2)
707 member = Member.find(1)
707 member = Member.find(1)
708 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
708 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
709
709
710 assert_equal %w(due_date start_date), issue.required_attribute_names(user).sort
710 assert_equal %w(due_date start_date), issue.required_attribute_names(user).sort
711
711
712 member.role_ids = [1, 2]
712 member.role_ids = [1, 2]
713 member.save!
713 member.save!
714 assert_equal [], issue.required_attribute_names(user.reload)
714 assert_equal [], issue.required_attribute_names(user.reload)
715
715
716 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
716 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
717 :role_id => 2, :field_name => 'due_date',
717 :role_id => 2, :field_name => 'due_date',
718 :rule => 'required')
718 :rule => 'required')
719 assert_equal %w(due_date), issue.required_attribute_names(user)
719 assert_equal %w(due_date), issue.required_attribute_names(user)
720
720
721 member.role_ids = [1, 2, 3]
721 member.role_ids = [1, 2, 3]
722 member.save!
722 member.save!
723 assert_equal [], issue.required_attribute_names(user.reload)
723 assert_equal [], issue.required_attribute_names(user.reload)
724
724
725 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
725 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
726 :role_id => 2, :field_name => 'due_date',
726 :role_id => 2, :field_name => 'due_date',
727 :rule => 'readonly')
727 :rule => 'readonly')
728 # required + readonly => required
728 # required + readonly => required
729 assert_equal %w(due_date), issue.required_attribute_names(user)
729 assert_equal %w(due_date), issue.required_attribute_names(user)
730 end
730 end
731
731
732 def test_read_only_attribute_names_for_multiple_roles_should_intersect_rules
732 def test_read_only_attribute_names_for_multiple_roles_should_intersect_rules
733 WorkflowPermission.delete_all
733 WorkflowPermission.delete_all
734 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
734 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
735 :role_id => 1, :field_name => 'due_date',
735 :role_id => 1, :field_name => 'due_date',
736 :rule => 'readonly')
736 :rule => 'readonly')
737 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
737 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
738 :role_id => 1, :field_name => 'start_date',
738 :role_id => 1, :field_name => 'start_date',
739 :rule => 'readonly')
739 :rule => 'readonly')
740 user = User.find(2)
740 user = User.find(2)
741 member = Member.find(1)
741 member = Member.find(1)
742 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
742 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
743
743
744 assert_equal %w(due_date start_date), issue.read_only_attribute_names(user).sort
744 assert_equal %w(due_date start_date), issue.read_only_attribute_names(user).sort
745
745
746 member.role_ids = [1, 2]
746 member.role_ids = [1, 2]
747 member.save!
747 member.save!
748 assert_equal [], issue.read_only_attribute_names(user.reload)
748 assert_equal [], issue.read_only_attribute_names(user.reload)
749
749
750 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
750 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
751 :role_id => 2, :field_name => 'due_date',
751 :role_id => 2, :field_name => 'due_date',
752 :rule => 'readonly')
752 :rule => 'readonly')
753 assert_equal %w(due_date), issue.read_only_attribute_names(user)
753 assert_equal %w(due_date), issue.read_only_attribute_names(user)
754 end
754 end
755
755
756 def test_copy
756 def test_copy
757 issue = Issue.new.copy_from(1)
757 issue = Issue.new.copy_from(1)
758 assert issue.copy?
758 assert issue.copy?
759 assert issue.save
759 assert issue.save
760 issue.reload
760 issue.reload
761 orig = Issue.find(1)
761 orig = Issue.find(1)
762 assert_equal orig.subject, issue.subject
762 assert_equal orig.subject, issue.subject
763 assert_equal orig.tracker, issue.tracker
763 assert_equal orig.tracker, issue.tracker
764 assert_equal "125", issue.custom_value_for(2).value
764 assert_equal "125", issue.custom_value_for(2).value
765 end
765 end
766
766
767 def test_copy_should_copy_status
767 def test_copy_should_copy_status
768 orig = Issue.find(8)
768 orig = Issue.find(8)
769 assert orig.status != IssueStatus.default
769 assert orig.status != IssueStatus.default
770
770
771 issue = Issue.new.copy_from(orig)
771 issue = Issue.new.copy_from(orig)
772 assert issue.save
772 assert issue.save
773 issue.reload
773 issue.reload
774 assert_equal orig.status, issue.status
774 assert_equal orig.status, issue.status
775 end
775 end
776
776
777 def test_copy_should_add_relation_with_copied_issue
777 def test_copy_should_add_relation_with_copied_issue
778 copied = Issue.find(1)
778 copied = Issue.find(1)
779 issue = Issue.new.copy_from(copied)
779 issue = Issue.new.copy_from(copied)
780 assert issue.save
780 assert issue.save
781 issue.reload
781 issue.reload
782
782
783 assert_equal 1, issue.relations.size
783 assert_equal 1, issue.relations.size
784 relation = issue.relations.first
784 relation = issue.relations.first
785 assert_equal 'copied_to', relation.relation_type
785 assert_equal 'copied_to', relation.relation_type
786 assert_equal copied, relation.issue_from
786 assert_equal copied, relation.issue_from
787 assert_equal issue, relation.issue_to
787 assert_equal issue, relation.issue_to
788 end
788 end
789
789
790 def test_copy_should_copy_subtasks
790 def test_copy_should_copy_subtasks
791 issue = Issue.generate_with_descendants!
791 issue = Issue.generate_with_descendants!
792
792
793 copy = issue.reload.copy
793 copy = issue.reload.copy
794 copy.author = User.find(7)
794 copy.author = User.find(7)
795 assert_difference 'Issue.count', 1+issue.descendants.count do
795 assert_difference 'Issue.count', 1+issue.descendants.count do
796 assert copy.save
796 assert copy.save
797 end
797 end
798 copy.reload
798 copy.reload
799 assert_equal %w(Child1 Child2), copy.children.map(&:subject).sort
799 assert_equal %w(Child1 Child2), copy.children.map(&:subject).sort
800 child_copy = copy.children.detect {|c| c.subject == 'Child1'}
800 child_copy = copy.children.detect {|c| c.subject == 'Child1'}
801 assert_equal %w(Child11), child_copy.children.map(&:subject).sort
801 assert_equal %w(Child11), child_copy.children.map(&:subject).sort
802 assert_equal copy.author, child_copy.author
802 assert_equal copy.author, child_copy.author
803 end
803 end
804
804
805 def test_copy_should_copy_subtasks_to_target_project
805 def test_copy_should_copy_subtasks_to_target_project
806 issue = Issue.generate_with_descendants!
806 issue = Issue.generate_with_descendants!
807
807
808 copy = issue.copy(:project_id => 3)
808 copy = issue.copy(:project_id => 3)
809 assert_difference 'Issue.count', 1+issue.descendants.count do
809 assert_difference 'Issue.count', 1+issue.descendants.count do
810 assert copy.save
810 assert copy.save
811 end
811 end
812 assert_equal [3], copy.reload.descendants.map(&:project_id).uniq
812 assert_equal [3], copy.reload.descendants.map(&:project_id).uniq
813 end
813 end
814
814
815 def test_copy_should_not_copy_subtasks_twice_when_saving_twice
815 def test_copy_should_not_copy_subtasks_twice_when_saving_twice
816 issue = Issue.generate_with_descendants!
816 issue = Issue.generate_with_descendants!
817
817
818 copy = issue.reload.copy
818 copy = issue.reload.copy
819 assert_difference 'Issue.count', 1+issue.descendants.count do
819 assert_difference 'Issue.count', 1+issue.descendants.count do
820 assert copy.save
820 assert copy.save
821 assert copy.save
821 assert copy.save
822 end
822 end
823 end
823 end
824
824
825 def test_should_not_call_after_project_change_on_creation
825 def test_should_not_call_after_project_change_on_creation
826 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1,
826 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1,
827 :subject => 'Test', :author_id => 1)
827 :subject => 'Test', :author_id => 1)
828 issue.expects(:after_project_change).never
828 issue.expects(:after_project_change).never
829 issue.save!
829 issue.save!
830 end
830 end
831
831
832 def test_should_not_call_after_project_change_on_update
832 def test_should_not_call_after_project_change_on_update
833 issue = Issue.find(1)
833 issue = Issue.find(1)
834 issue.project = Project.find(1)
834 issue.project = Project.find(1)
835 issue.subject = 'No project change'
835 issue.subject = 'No project change'
836 issue.expects(:after_project_change).never
836 issue.expects(:after_project_change).never
837 issue.save!
837 issue.save!
838 end
838 end
839
839
840 def test_should_call_after_project_change_on_project_change
840 def test_should_call_after_project_change_on_project_change
841 issue = Issue.find(1)
841 issue = Issue.find(1)
842 issue.project = Project.find(2)
842 issue.project = Project.find(2)
843 issue.expects(:after_project_change).once
843 issue.expects(:after_project_change).once
844 issue.save!
844 issue.save!
845 end
845 end
846
846
847 def test_adding_journal_should_update_timestamp
847 def test_adding_journal_should_update_timestamp
848 issue = Issue.find(1)
848 issue = Issue.find(1)
849 updated_on_was = issue.updated_on
849 updated_on_was = issue.updated_on
850
850
851 issue.init_journal(User.first, "Adding notes")
851 issue.init_journal(User.first, "Adding notes")
852 assert_difference 'Journal.count' do
852 assert_difference 'Journal.count' do
853 assert issue.save
853 assert issue.save
854 end
854 end
855 issue.reload
855 issue.reload
856
856
857 assert_not_equal updated_on_was, issue.updated_on
857 assert_not_equal updated_on_was, issue.updated_on
858 end
858 end
859
859
860 def test_should_close_duplicates
860 def test_should_close_duplicates
861 # Create 3 issues
861 # Create 3 issues
862 issue1 = Issue.generate!
862 issue1 = Issue.generate!
863 issue2 = Issue.generate!
863 issue2 = Issue.generate!
864 issue3 = Issue.generate!
864 issue3 = Issue.generate!
865
865
866 # 2 is a dupe of 1
866 # 2 is a dupe of 1
867 IssueRelation.create!(:issue_from => issue2, :issue_to => issue1,
867 IssueRelation.create!(:issue_from => issue2, :issue_to => issue1,
868 :relation_type => IssueRelation::TYPE_DUPLICATES)
868 :relation_type => IssueRelation::TYPE_DUPLICATES)
869 # And 3 is a dupe of 2
869 # And 3 is a dupe of 2
870 IssueRelation.create!(:issue_from => issue3, :issue_to => issue2,
870 IssueRelation.create!(:issue_from => issue3, :issue_to => issue2,
871 :relation_type => IssueRelation::TYPE_DUPLICATES)
871 :relation_type => IssueRelation::TYPE_DUPLICATES)
872 # And 3 is a dupe of 1 (circular duplicates)
872 # And 3 is a dupe of 1 (circular duplicates)
873 IssueRelation.create!(:issue_from => issue3, :issue_to => issue1,
873 IssueRelation.create!(:issue_from => issue3, :issue_to => issue1,
874 :relation_type => IssueRelation::TYPE_DUPLICATES)
874 :relation_type => IssueRelation::TYPE_DUPLICATES)
875
875
876 assert issue1.reload.duplicates.include?(issue2)
876 assert issue1.reload.duplicates.include?(issue2)
877
877
878 # Closing issue 1
878 # Closing issue 1
879 issue1.init_journal(User.find(:first), "Closing issue1")
879 issue1.init_journal(User.find(:first), "Closing issue1")
880 issue1.status = IssueStatus.find :first, :conditions => {:is_closed => true}
880 issue1.status = IssueStatus.find :first, :conditions => {:is_closed => true}
881 assert issue1.save
881 assert issue1.save
882 # 2 and 3 should be also closed
882 # 2 and 3 should be also closed
883 assert issue2.reload.closed?
883 assert issue2.reload.closed?
884 assert issue3.reload.closed?
884 assert issue3.reload.closed?
885 end
885 end
886
886
887 def test_should_not_close_duplicated_issue
887 def test_should_not_close_duplicated_issue
888 issue1 = Issue.generate!
888 issue1 = Issue.generate!
889 issue2 = Issue.generate!
889 issue2 = Issue.generate!
890
890
891 # 2 is a dupe of 1
891 # 2 is a dupe of 1
892 IssueRelation.create(:issue_from => issue2, :issue_to => issue1,
892 IssueRelation.create(:issue_from => issue2, :issue_to => issue1,
893 :relation_type => IssueRelation::TYPE_DUPLICATES)
893 :relation_type => IssueRelation::TYPE_DUPLICATES)
894 # 2 is a dup of 1 but 1 is not a duplicate of 2
894 # 2 is a dup of 1 but 1 is not a duplicate of 2
895 assert !issue2.reload.duplicates.include?(issue1)
895 assert !issue2.reload.duplicates.include?(issue1)
896
896
897 # Closing issue 2
897 # Closing issue 2
898 issue2.init_journal(User.find(:first), "Closing issue2")
898 issue2.init_journal(User.find(:first), "Closing issue2")
899 issue2.status = IssueStatus.find :first, :conditions => {:is_closed => true}
899 issue2.status = IssueStatus.find :first, :conditions => {:is_closed => true}
900 assert issue2.save
900 assert issue2.save
901 # 1 should not be also closed
901 # 1 should not be also closed
902 assert !issue1.reload.closed?
902 assert !issue1.reload.closed?
903 end
903 end
904
904
905 def test_assignable_versions
905 def test_assignable_versions
906 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
906 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
907 :status_id => 1, :fixed_version_id => 1,
907 :status_id => 1, :fixed_version_id => 1,
908 :subject => 'New issue')
908 :subject => 'New issue')
909 assert_equal ['open'], issue.assignable_versions.collect(&:status).uniq
909 assert_equal ['open'], issue.assignable_versions.collect(&:status).uniq
910 end
910 end
911
911
912 def test_should_not_be_able_to_assign_a_new_issue_to_a_closed_version
912 def test_should_not_be_able_to_assign_a_new_issue_to_a_closed_version
913 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
913 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
914 :status_id => 1, :fixed_version_id => 1,
914 :status_id => 1, :fixed_version_id => 1,
915 :subject => 'New issue')
915 :subject => 'New issue')
916 assert !issue.save
916 assert !issue.save
917 assert_not_nil issue.errors[:fixed_version_id]
917 assert_not_nil issue.errors[:fixed_version_id]
918 end
918 end
919
919
920 def test_should_not_be_able_to_assign_a_new_issue_to_a_locked_version
920 def test_should_not_be_able_to_assign_a_new_issue_to_a_locked_version
921 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
921 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
922 :status_id => 1, :fixed_version_id => 2,
922 :status_id => 1, :fixed_version_id => 2,
923 :subject => 'New issue')
923 :subject => 'New issue')
924 assert !issue.save
924 assert !issue.save
925 assert_not_nil issue.errors[:fixed_version_id]
925 assert_not_nil issue.errors[:fixed_version_id]
926 end
926 end
927
927
928 def test_should_be_able_to_assign_a_new_issue_to_an_open_version
928 def test_should_be_able_to_assign_a_new_issue_to_an_open_version
929 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
929 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
930 :status_id => 1, :fixed_version_id => 3,
930 :status_id => 1, :fixed_version_id => 3,
931 :subject => 'New issue')
931 :subject => 'New issue')
932 assert issue.save
932 assert issue.save
933 end
933 end
934
934
935 def test_should_be_able_to_update_an_issue_assigned_to_a_closed_version
935 def test_should_be_able_to_update_an_issue_assigned_to_a_closed_version
936 issue = Issue.find(11)
936 issue = Issue.find(11)
937 assert_equal 'closed', issue.fixed_version.status
937 assert_equal 'closed', issue.fixed_version.status
938 issue.subject = 'Subject changed'
938 issue.subject = 'Subject changed'
939 assert issue.save
939 assert issue.save
940 end
940 end
941
941
942 def test_should_not_be_able_to_reopen_an_issue_assigned_to_a_closed_version
942 def test_should_not_be_able_to_reopen_an_issue_assigned_to_a_closed_version
943 issue = Issue.find(11)
943 issue = Issue.find(11)
944 issue.status_id = 1
944 issue.status_id = 1
945 assert !issue.save
945 assert !issue.save
946 assert_not_nil issue.errors[:base]
946 assert_not_nil issue.errors[:base]
947 end
947 end
948
948
949 def test_should_be_able_to_reopen_and_reassign_an_issue_assigned_to_a_closed_version
949 def test_should_be_able_to_reopen_and_reassign_an_issue_assigned_to_a_closed_version
950 issue = Issue.find(11)
950 issue = Issue.find(11)
951 issue.status_id = 1
951 issue.status_id = 1
952 issue.fixed_version_id = 3
952 issue.fixed_version_id = 3
953 assert issue.save
953 assert issue.save
954 end
954 end
955
955
956 def test_should_be_able_to_reopen_an_issue_assigned_to_a_locked_version
956 def test_should_be_able_to_reopen_an_issue_assigned_to_a_locked_version
957 issue = Issue.find(12)
957 issue = Issue.find(12)
958 assert_equal 'locked', issue.fixed_version.status
958 assert_equal 'locked', issue.fixed_version.status
959 issue.status_id = 1
959 issue.status_id = 1
960 assert issue.save
960 assert issue.save
961 end
961 end
962
962
963 def test_should_not_be_able_to_keep_unshared_version_when_changing_project
963 def test_should_not_be_able_to_keep_unshared_version_when_changing_project
964 issue = Issue.find(2)
964 issue = Issue.find(2)
965 assert_equal 2, issue.fixed_version_id
965 assert_equal 2, issue.fixed_version_id
966 issue.project_id = 3
966 issue.project_id = 3
967 assert_nil issue.fixed_version_id
967 assert_nil issue.fixed_version_id
968 issue.fixed_version_id = 2
968 issue.fixed_version_id = 2
969 assert !issue.save
969 assert !issue.save
970 assert_include 'Target version is not included in the list', issue.errors.full_messages
970 assert_include 'Target version is not included in the list', issue.errors.full_messages
971 end
971 end
972
972
973 def test_should_keep_shared_version_when_changing_project
973 def test_should_keep_shared_version_when_changing_project
974 Version.find(2).update_attribute :sharing, 'tree'
974 Version.find(2).update_attribute :sharing, 'tree'
975
975
976 issue = Issue.find(2)
976 issue = Issue.find(2)
977 assert_equal 2, issue.fixed_version_id
977 assert_equal 2, issue.fixed_version_id
978 issue.project_id = 3
978 issue.project_id = 3
979 assert_equal 2, issue.fixed_version_id
979 assert_equal 2, issue.fixed_version_id
980 assert issue.save
980 assert issue.save
981 end
981 end
982
982
983 def test_allowed_target_projects_on_move_should_include_projects_with_issue_tracking_enabled
983 def test_allowed_target_projects_on_move_should_include_projects_with_issue_tracking_enabled
984 assert_include Project.find(2), Issue.allowed_target_projects_on_move(User.find(2))
984 assert_include Project.find(2), Issue.allowed_target_projects_on_move(User.find(2))
985 end
985 end
986
986
987 def test_allowed_target_projects_on_move_should_not_include_projects_with_issue_tracking_disabled
987 def test_allowed_target_projects_on_move_should_not_include_projects_with_issue_tracking_disabled
988 Project.find(2).disable_module! :issue_tracking
988 Project.find(2).disable_module! :issue_tracking
989 assert_not_include Project.find(2), Issue.allowed_target_projects_on_move(User.find(2))
989 assert_not_include Project.find(2), Issue.allowed_target_projects_on_move(User.find(2))
990 end
990 end
991
991
992 def test_move_to_another_project_with_same_category
992 def test_move_to_another_project_with_same_category
993 issue = Issue.find(1)
993 issue = Issue.find(1)
994 issue.project = Project.find(2)
994 issue.project = Project.find(2)
995 assert issue.save
995 assert issue.save
996 issue.reload
996 issue.reload
997 assert_equal 2, issue.project_id
997 assert_equal 2, issue.project_id
998 # Category changes
998 # Category changes
999 assert_equal 4, issue.category_id
999 assert_equal 4, issue.category_id
1000 # Make sure time entries were move to the target project
1000 # Make sure time entries were move to the target project
1001 assert_equal 2, issue.time_entries.first.project_id
1001 assert_equal 2, issue.time_entries.first.project_id
1002 end
1002 end
1003
1003
1004 def test_move_to_another_project_without_same_category
1004 def test_move_to_another_project_without_same_category
1005 issue = Issue.find(2)
1005 issue = Issue.find(2)
1006 issue.project = Project.find(2)
1006 issue.project = Project.find(2)
1007 assert issue.save
1007 assert issue.save
1008 issue.reload
1008 issue.reload
1009 assert_equal 2, issue.project_id
1009 assert_equal 2, issue.project_id
1010 # Category cleared
1010 # Category cleared
1011 assert_nil issue.category_id
1011 assert_nil issue.category_id
1012 end
1012 end
1013
1013
1014 def test_move_to_another_project_should_clear_fixed_version_when_not_shared
1014 def test_move_to_another_project_should_clear_fixed_version_when_not_shared
1015 issue = Issue.find(1)
1015 issue = Issue.find(1)
1016 issue.update_attribute(:fixed_version_id, 1)
1016 issue.update_attribute(:fixed_version_id, 1)
1017 issue.project = Project.find(2)
1017 issue.project = Project.find(2)
1018 assert issue.save
1018 assert issue.save
1019 issue.reload
1019 issue.reload
1020 assert_equal 2, issue.project_id
1020 assert_equal 2, issue.project_id
1021 # Cleared fixed_version
1021 # Cleared fixed_version
1022 assert_equal nil, issue.fixed_version
1022 assert_equal nil, issue.fixed_version
1023 end
1023 end
1024
1024
1025 def test_move_to_another_project_should_keep_fixed_version_when_shared_with_the_target_project
1025 def test_move_to_another_project_should_keep_fixed_version_when_shared_with_the_target_project
1026 issue = Issue.find(1)
1026 issue = Issue.find(1)
1027 issue.update_attribute(:fixed_version_id, 4)
1027 issue.update_attribute(:fixed_version_id, 4)
1028 issue.project = Project.find(5)
1028 issue.project = Project.find(5)
1029 assert issue.save
1029 assert issue.save
1030 issue.reload
1030 issue.reload
1031 assert_equal 5, issue.project_id
1031 assert_equal 5, issue.project_id
1032 # Keep fixed_version
1032 # Keep fixed_version
1033 assert_equal 4, issue.fixed_version_id
1033 assert_equal 4, issue.fixed_version_id
1034 end
1034 end
1035
1035
1036 def test_move_to_another_project_should_clear_fixed_version_when_not_shared_with_the_target_project
1036 def test_move_to_another_project_should_clear_fixed_version_when_not_shared_with_the_target_project
1037 issue = Issue.find(1)
1037 issue = Issue.find(1)
1038 issue.update_attribute(:fixed_version_id, 1)
1038 issue.update_attribute(:fixed_version_id, 1)
1039 issue.project = Project.find(5)
1039 issue.project = Project.find(5)
1040 assert issue.save
1040 assert issue.save
1041 issue.reload
1041 issue.reload
1042 assert_equal 5, issue.project_id
1042 assert_equal 5, issue.project_id
1043 # Cleared fixed_version
1043 # Cleared fixed_version
1044 assert_equal nil, issue.fixed_version
1044 assert_equal nil, issue.fixed_version
1045 end
1045 end
1046
1046
1047 def test_move_to_another_project_should_keep_fixed_version_when_shared_systemwide
1047 def test_move_to_another_project_should_keep_fixed_version_when_shared_systemwide
1048 issue = Issue.find(1)
1048 issue = Issue.find(1)
1049 issue.update_attribute(:fixed_version_id, 7)
1049 issue.update_attribute(:fixed_version_id, 7)
1050 issue.project = Project.find(2)
1050 issue.project = Project.find(2)
1051 assert issue.save
1051 assert issue.save
1052 issue.reload
1052 issue.reload
1053 assert_equal 2, issue.project_id
1053 assert_equal 2, issue.project_id
1054 # Keep fixed_version
1054 # Keep fixed_version
1055 assert_equal 7, issue.fixed_version_id
1055 assert_equal 7, issue.fixed_version_id
1056 end
1056 end
1057
1057
1058 def test_move_to_another_project_should_keep_parent_if_valid
1058 def test_move_to_another_project_should_keep_parent_if_valid
1059 issue = Issue.find(1)
1059 issue = Issue.find(1)
1060 issue.update_attribute(:parent_issue_id, 2)
1060 issue.update_attribute(:parent_issue_id, 2)
1061 issue.project = Project.find(3)
1061 issue.project = Project.find(3)
1062 assert issue.save
1062 assert issue.save
1063 issue.reload
1063 issue.reload
1064 assert_equal 2, issue.parent_id
1064 assert_equal 2, issue.parent_id
1065 end
1065 end
1066
1066
1067 def test_move_to_another_project_should_clear_parent_if_not_valid
1067 def test_move_to_another_project_should_clear_parent_if_not_valid
1068 issue = Issue.find(1)
1068 issue = Issue.find(1)
1069 issue.update_attribute(:parent_issue_id, 2)
1069 issue.update_attribute(:parent_issue_id, 2)
1070 issue.project = Project.find(2)
1070 issue.project = Project.find(2)
1071 assert issue.save
1071 assert issue.save
1072 issue.reload
1072 issue.reload
1073 assert_nil issue.parent_id
1073 assert_nil issue.parent_id
1074 end
1074 end
1075
1075
1076 def test_move_to_another_project_with_disabled_tracker
1076 def test_move_to_another_project_with_disabled_tracker
1077 issue = Issue.find(1)
1077 issue = Issue.find(1)
1078 target = Project.find(2)
1078 target = Project.find(2)
1079 target.tracker_ids = [3]
1079 target.tracker_ids = [3]
1080 target.save
1080 target.save
1081 issue.project = target
1081 issue.project = target
1082 assert issue.save
1082 assert issue.save
1083 issue.reload
1083 issue.reload
1084 assert_equal 2, issue.project_id
1084 assert_equal 2, issue.project_id
1085 assert_equal 3, issue.tracker_id
1085 assert_equal 3, issue.tracker_id
1086 end
1086 end
1087
1087
1088 def test_copy_to_the_same_project
1088 def test_copy_to_the_same_project
1089 issue = Issue.find(1)
1089 issue = Issue.find(1)
1090 copy = issue.copy
1090 copy = issue.copy
1091 assert_difference 'Issue.count' do
1091 assert_difference 'Issue.count' do
1092 copy.save!
1092 copy.save!
1093 end
1093 end
1094 assert_kind_of Issue, copy
1094 assert_kind_of Issue, copy
1095 assert_equal issue.project, copy.project
1095 assert_equal issue.project, copy.project
1096 assert_equal "125", copy.custom_value_for(2).value
1096 assert_equal "125", copy.custom_value_for(2).value
1097 end
1097 end
1098
1098
1099 def test_copy_to_another_project_and_tracker
1099 def test_copy_to_another_project_and_tracker
1100 issue = Issue.find(1)
1100 issue = Issue.find(1)
1101 copy = issue.copy(:project_id => 3, :tracker_id => 2)
1101 copy = issue.copy(:project_id => 3, :tracker_id => 2)
1102 assert_difference 'Issue.count' do
1102 assert_difference 'Issue.count' do
1103 copy.save!
1103 copy.save!
1104 end
1104 end
1105 copy.reload
1105 copy.reload
1106 assert_kind_of Issue, copy
1106 assert_kind_of Issue, copy
1107 assert_equal Project.find(3), copy.project
1107 assert_equal Project.find(3), copy.project
1108 assert_equal Tracker.find(2), copy.tracker
1108 assert_equal Tracker.find(2), copy.tracker
1109 # Custom field #2 is not associated with target tracker
1109 # Custom field #2 is not associated with target tracker
1110 assert_nil copy.custom_value_for(2)
1110 assert_nil copy.custom_value_for(2)
1111 end
1111 end
1112
1112
1113 context "#copy" do
1113 context "#copy" do
1114 setup do
1114 setup do
1115 @issue = Issue.find(1)
1115 @issue = Issue.find(1)
1116 end
1116 end
1117
1117
1118 should "not create a journal" do
1118 should "not create a journal" do
1119 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :assigned_to_id => 3)
1119 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :assigned_to_id => 3)
1120 copy.save!
1120 copy.save!
1121 assert_equal 0, copy.reload.journals.size
1121 assert_equal 0, copy.reload.journals.size
1122 end
1122 end
1123
1123
1124 should "allow assigned_to changes" do
1124 should "allow assigned_to changes" do
1125 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :assigned_to_id => 3)
1125 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :assigned_to_id => 3)
1126 assert_equal 3, copy.assigned_to_id
1126 assert_equal 3, copy.assigned_to_id
1127 end
1127 end
1128
1128
1129 should "allow status changes" do
1129 should "allow status changes" do
1130 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :status_id => 2)
1130 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :status_id => 2)
1131 assert_equal 2, copy.status_id
1131 assert_equal 2, copy.status_id
1132 end
1132 end
1133
1133
1134 should "allow start date changes" do
1134 should "allow start date changes" do
1135 date = Date.today
1135 date = Date.today
1136 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :start_date => date)
1136 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :start_date => date)
1137 assert_equal date, copy.start_date
1137 assert_equal date, copy.start_date
1138 end
1138 end
1139
1139
1140 should "allow due date changes" do
1140 should "allow due date changes" do
1141 date = Date.today
1141 date = Date.today
1142 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :due_date => date)
1142 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :due_date => date)
1143 assert_equal date, copy.due_date
1143 assert_equal date, copy.due_date
1144 end
1144 end
1145
1145
1146 should "set current user as author" do
1146 should "set current user as author" do
1147 User.current = User.find(9)
1147 User.current = User.find(9)
1148 copy = @issue.copy(:project_id => 3, :tracker_id => 2)
1148 copy = @issue.copy(:project_id => 3, :tracker_id => 2)
1149 assert_equal User.current, copy.author
1149 assert_equal User.current, copy.author
1150 end
1150 end
1151
1151
1152 should "create a journal with notes" do
1152 should "create a journal with notes" do
1153 date = Date.today
1153 date = Date.today
1154 notes = "Notes added when copying"
1154 notes = "Notes added when copying"
1155 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :start_date => date)
1155 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :start_date => date)
1156 copy.init_journal(User.current, notes)
1156 copy.init_journal(User.current, notes)
1157 copy.save!
1157 copy.save!
1158
1158
1159 assert_equal 1, copy.journals.size
1159 assert_equal 1, copy.journals.size
1160 journal = copy.journals.first
1160 journal = copy.journals.first
1161 assert_equal 0, journal.details.size
1161 assert_equal 0, journal.details.size
1162 assert_equal notes, journal.notes
1162 assert_equal notes, journal.notes
1163 end
1163 end
1164 end
1164 end
1165
1165
1166 def test_valid_parent_project
1166 def test_valid_parent_project
1167 issue = Issue.find(1)
1167 issue = Issue.find(1)
1168 issue_in_same_project = Issue.find(2)
1168 issue_in_same_project = Issue.find(2)
1169 issue_in_child_project = Issue.find(5)
1169 issue_in_child_project = Issue.find(5)
1170 issue_in_grandchild_project = Issue.generate!(:project_id => 6, :tracker_id => 1)
1170 issue_in_grandchild_project = Issue.generate!(:project_id => 6, :tracker_id => 1)
1171 issue_in_other_child_project = Issue.find(6)
1171 issue_in_other_child_project = Issue.find(6)
1172 issue_in_different_tree = Issue.find(4)
1172 issue_in_different_tree = Issue.find(4)
1173
1173
1174 with_settings :cross_project_subtasks => '' do
1174 with_settings :cross_project_subtasks => '' do
1175 assert_equal true, issue.valid_parent_project?(issue_in_same_project)
1175 assert_equal true, issue.valid_parent_project?(issue_in_same_project)
1176 assert_equal false, issue.valid_parent_project?(issue_in_child_project)
1176 assert_equal false, issue.valid_parent_project?(issue_in_child_project)
1177 assert_equal false, issue.valid_parent_project?(issue_in_grandchild_project)
1177 assert_equal false, issue.valid_parent_project?(issue_in_grandchild_project)
1178 assert_equal false, issue.valid_parent_project?(issue_in_different_tree)
1178 assert_equal false, issue.valid_parent_project?(issue_in_different_tree)
1179 end
1179 end
1180
1180
1181 with_settings :cross_project_subtasks => 'system' do
1181 with_settings :cross_project_subtasks => 'system' do
1182 assert_equal true, issue.valid_parent_project?(issue_in_same_project)
1182 assert_equal true, issue.valid_parent_project?(issue_in_same_project)
1183 assert_equal true, issue.valid_parent_project?(issue_in_child_project)
1183 assert_equal true, issue.valid_parent_project?(issue_in_child_project)
1184 assert_equal true, issue.valid_parent_project?(issue_in_different_tree)
1184 assert_equal true, issue.valid_parent_project?(issue_in_different_tree)
1185 end
1185 end
1186
1186
1187 with_settings :cross_project_subtasks => 'tree' do
1187 with_settings :cross_project_subtasks => 'tree' do
1188 assert_equal true, issue.valid_parent_project?(issue_in_same_project)
1188 assert_equal true, issue.valid_parent_project?(issue_in_same_project)
1189 assert_equal true, issue.valid_parent_project?(issue_in_child_project)
1189 assert_equal true, issue.valid_parent_project?(issue_in_child_project)
1190 assert_equal true, issue.valid_parent_project?(issue_in_grandchild_project)
1190 assert_equal true, issue.valid_parent_project?(issue_in_grandchild_project)
1191 assert_equal false, issue.valid_parent_project?(issue_in_different_tree)
1191 assert_equal false, issue.valid_parent_project?(issue_in_different_tree)
1192
1192
1193 assert_equal true, issue_in_child_project.valid_parent_project?(issue_in_same_project)
1193 assert_equal true, issue_in_child_project.valid_parent_project?(issue_in_same_project)
1194 assert_equal true, issue_in_child_project.valid_parent_project?(issue_in_other_child_project)
1194 assert_equal true, issue_in_child_project.valid_parent_project?(issue_in_other_child_project)
1195 end
1195 end
1196
1196
1197 with_settings :cross_project_subtasks => 'descendants' do
1197 with_settings :cross_project_subtasks => 'descendants' do
1198 assert_equal true, issue.valid_parent_project?(issue_in_same_project)
1198 assert_equal true, issue.valid_parent_project?(issue_in_same_project)
1199 assert_equal false, issue.valid_parent_project?(issue_in_child_project)
1199 assert_equal false, issue.valid_parent_project?(issue_in_child_project)
1200 assert_equal false, issue.valid_parent_project?(issue_in_grandchild_project)
1200 assert_equal false, issue.valid_parent_project?(issue_in_grandchild_project)
1201 assert_equal false, issue.valid_parent_project?(issue_in_different_tree)
1201 assert_equal false, issue.valid_parent_project?(issue_in_different_tree)
1202
1202
1203 assert_equal true, issue_in_child_project.valid_parent_project?(issue)
1203 assert_equal true, issue_in_child_project.valid_parent_project?(issue)
1204 assert_equal false, issue_in_child_project.valid_parent_project?(issue_in_other_child_project)
1204 assert_equal false, issue_in_child_project.valid_parent_project?(issue_in_other_child_project)
1205 end
1205 end
1206 end
1206 end
1207
1207
1208 def test_recipients_should_include_previous_assignee
1208 def test_recipients_should_include_previous_assignee
1209 user = User.find(3)
1209 user = User.find(3)
1210 user.members.update_all ["mail_notification = ?", false]
1210 user.members.update_all ["mail_notification = ?", false]
1211 user.update_attribute :mail_notification, 'only_assigned'
1211 user.update_attribute :mail_notification, 'only_assigned'
1212
1212
1213 issue = Issue.find(2)
1213 issue = Issue.find(2)
1214 issue.assigned_to = nil
1214 issue.assigned_to = nil
1215 assert_include user.mail, issue.recipients
1215 assert_include user.mail, issue.recipients
1216 issue.save!
1216 issue.save!
1217 assert !issue.recipients.include?(user.mail)
1217 assert !issue.recipients.include?(user.mail)
1218 end
1218 end
1219
1219
1220 def test_recipients_should_not_include_users_that_cannot_view_the_issue
1220 def test_recipients_should_not_include_users_that_cannot_view_the_issue
1221 issue = Issue.find(12)
1221 issue = Issue.find(12)
1222 assert issue.recipients.include?(issue.author.mail)
1222 assert issue.recipients.include?(issue.author.mail)
1223 # copy the issue to a private project
1223 # copy the issue to a private project
1224 copy = issue.copy(:project_id => 5, :tracker_id => 2)
1224 copy = issue.copy(:project_id => 5, :tracker_id => 2)
1225 # author is not a member of project anymore
1225 # author is not a member of project anymore
1226 assert !copy.recipients.include?(copy.author.mail)
1226 assert !copy.recipients.include?(copy.author.mail)
1227 end
1227 end
1228
1228
1229 def test_recipients_should_include_the_assigned_group_members
1229 def test_recipients_should_include_the_assigned_group_members
1230 group_member = User.generate!
1230 group_member = User.generate!
1231 group = Group.generate!
1231 group = Group.generate!
1232 group.users << group_member
1232 group.users << group_member
1233
1233
1234 issue = Issue.find(12)
1234 issue = Issue.find(12)
1235 issue.assigned_to = group
1235 issue.assigned_to = group
1236 assert issue.recipients.include?(group_member.mail)
1236 assert issue.recipients.include?(group_member.mail)
1237 end
1237 end
1238
1238
1239 def test_watcher_recipients_should_not_include_users_that_cannot_view_the_issue
1239 def test_watcher_recipients_should_not_include_users_that_cannot_view_the_issue
1240 user = User.find(3)
1240 user = User.find(3)
1241 issue = Issue.find(9)
1241 issue = Issue.find(9)
1242 Watcher.create!(:user => user, :watchable => issue)
1242 Watcher.create!(:user => user, :watchable => issue)
1243 assert issue.watched_by?(user)
1243 assert issue.watched_by?(user)
1244 assert !issue.watcher_recipients.include?(user.mail)
1244 assert !issue.watcher_recipients.include?(user.mail)
1245 end
1245 end
1246
1246
1247 def test_issue_destroy
1247 def test_issue_destroy
1248 Issue.find(1).destroy
1248 Issue.find(1).destroy
1249 assert_nil Issue.find_by_id(1)
1249 assert_nil Issue.find_by_id(1)
1250 assert_nil TimeEntry.find_by_issue_id(1)
1250 assert_nil TimeEntry.find_by_issue_id(1)
1251 end
1251 end
1252
1252
1253 def test_destroying_a_deleted_issue_should_not_raise_an_error
1253 def test_destroying_a_deleted_issue_should_not_raise_an_error
1254 issue = Issue.find(1)
1254 issue = Issue.find(1)
1255 Issue.find(1).destroy
1255 Issue.find(1).destroy
1256
1256
1257 assert_nothing_raised do
1257 assert_nothing_raised do
1258 assert_no_difference 'Issue.count' do
1258 assert_no_difference 'Issue.count' do
1259 issue.destroy
1259 issue.destroy
1260 end
1260 end
1261 assert issue.destroyed?
1261 assert issue.destroyed?
1262 end
1262 end
1263 end
1263 end
1264
1264
1265 def test_destroying_a_stale_issue_should_not_raise_an_error
1265 def test_destroying_a_stale_issue_should_not_raise_an_error
1266 issue = Issue.find(1)
1266 issue = Issue.find(1)
1267 Issue.find(1).update_attribute :subject, "Updated"
1267 Issue.find(1).update_attribute :subject, "Updated"
1268
1268
1269 assert_nothing_raised do
1269 assert_nothing_raised do
1270 assert_difference 'Issue.count', -1 do
1270 assert_difference 'Issue.count', -1 do
1271 issue.destroy
1271 issue.destroy
1272 end
1272 end
1273 assert issue.destroyed?
1273 assert issue.destroyed?
1274 end
1274 end
1275 end
1275 end
1276
1276
1277 def test_blocked
1277 def test_blocked
1278 blocked_issue = Issue.find(9)
1278 blocked_issue = Issue.find(9)
1279 blocking_issue = Issue.find(10)
1279 blocking_issue = Issue.find(10)
1280
1280
1281 assert blocked_issue.blocked?
1281 assert blocked_issue.blocked?
1282 assert !blocking_issue.blocked?
1282 assert !blocking_issue.blocked?
1283 end
1283 end
1284
1284
1285 def test_blocked_issues_dont_allow_closed_statuses
1285 def test_blocked_issues_dont_allow_closed_statuses
1286 blocked_issue = Issue.find(9)
1286 blocked_issue = Issue.find(9)
1287
1287
1288 allowed_statuses = blocked_issue.new_statuses_allowed_to(users(:users_002))
1288 allowed_statuses = blocked_issue.new_statuses_allowed_to(users(:users_002))
1289 assert !allowed_statuses.empty?
1289 assert !allowed_statuses.empty?
1290 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
1290 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
1291 assert closed_statuses.empty?
1291 assert closed_statuses.empty?
1292 end
1292 end
1293
1293
1294 def test_unblocked_issues_allow_closed_statuses
1294 def test_unblocked_issues_allow_closed_statuses
1295 blocking_issue = Issue.find(10)
1295 blocking_issue = Issue.find(10)
1296
1296
1297 allowed_statuses = blocking_issue.new_statuses_allowed_to(users(:users_002))
1297 allowed_statuses = blocking_issue.new_statuses_allowed_to(users(:users_002))
1298 assert !allowed_statuses.empty?
1298 assert !allowed_statuses.empty?
1299 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
1299 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
1300 assert !closed_statuses.empty?
1300 assert !closed_statuses.empty?
1301 end
1301 end
1302
1302
1303 def test_rescheduling_an_issue_should_reschedule_following_issue
1303 def test_rescheduling_an_issue_should_reschedule_following_issue
1304 issue1 = Issue.create!(:project_id => 1, :tracker_id => 1,
1304 issue1 = Issue.create!(:project_id => 1, :tracker_id => 1,
1305 :author_id => 1, :status_id => 1,
1305 :author_id => 1, :status_id => 1,
1306 :subject => '-',
1306 :subject => '-',
1307 :start_date => Date.today, :due_date => Date.today + 2)
1307 :start_date => Date.today, :due_date => Date.today + 2)
1308 issue2 = Issue.create!(:project_id => 1, :tracker_id => 1,
1308 issue2 = Issue.create!(:project_id => 1, :tracker_id => 1,
1309 :author_id => 1, :status_id => 1,
1309 :author_id => 1, :status_id => 1,
1310 :subject => '-',
1310 :subject => '-',
1311 :start_date => Date.today, :due_date => Date.today + 2)
1311 :start_date => Date.today, :due_date => Date.today + 2)
1312 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2,
1312 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2,
1313 :relation_type => IssueRelation::TYPE_PRECEDES)
1313 :relation_type => IssueRelation::TYPE_PRECEDES)
1314 assert_equal issue1.due_date + 1, issue2.reload.start_date
1314 assert_equal issue1.due_date + 1, issue2.reload.start_date
1315
1315
1316 issue1.due_date = Date.today + 5
1316 issue1.due_date = Date.today + 5
1317 issue1.save!
1317 issue1.save!
1318 assert_equal issue1.due_date + 1, issue2.reload.start_date
1318 assert_equal issue1.due_date + 1, issue2.reload.start_date
1319 end
1319 end
1320
1320
1321 def test_rescheduling_a_stale_issue_should_not_raise_an_error
1321 def test_rescheduling_a_stale_issue_should_not_raise_an_error
1322 stale = Issue.find(1)
1322 stale = Issue.find(1)
1323 issue = Issue.find(1)
1323 issue = Issue.find(1)
1324 issue.subject = "Updated"
1324 issue.subject = "Updated"
1325 issue.save!
1325 issue.save!
1326
1326
1327 date = 10.days.from_now.to_date
1327 date = 10.days.from_now.to_date
1328 assert_nothing_raised do
1328 assert_nothing_raised do
1329 stale.reschedule_after(date)
1329 stale.reschedule_after(date)
1330 end
1330 end
1331 assert_equal date, stale.reload.start_date
1331 assert_equal date, stale.reload.start_date
1332 end
1332 end
1333
1333
1334 def test_overdue
1334 def test_overdue
1335 assert Issue.new(:due_date => 1.day.ago.to_date).overdue?
1335 assert Issue.new(:due_date => 1.day.ago.to_date).overdue?
1336 assert !Issue.new(:due_date => Date.today).overdue?
1336 assert !Issue.new(:due_date => Date.today).overdue?
1337 assert !Issue.new(:due_date => 1.day.from_now.to_date).overdue?
1337 assert !Issue.new(:due_date => 1.day.from_now.to_date).overdue?
1338 assert !Issue.new(:due_date => nil).overdue?
1338 assert !Issue.new(:due_date => nil).overdue?
1339 assert !Issue.new(:due_date => 1.day.ago.to_date,
1339 assert !Issue.new(:due_date => 1.day.ago.to_date,
1340 :status => IssueStatus.find(:first,
1340 :status => IssueStatus.find(:first,
1341 :conditions => {:is_closed => true})
1341 :conditions => {:is_closed => true})
1342 ).overdue?
1342 ).overdue?
1343 end
1343 end
1344
1344
1345 context "#behind_schedule?" do
1345 context "#behind_schedule?" do
1346 should "be false if the issue has no start_date" do
1346 should "be false if the issue has no start_date" do
1347 assert !Issue.new(:start_date => nil,
1347 assert !Issue.new(:start_date => nil,
1348 :due_date => 1.day.from_now.to_date,
1348 :due_date => 1.day.from_now.to_date,
1349 :done_ratio => 0).behind_schedule?
1349 :done_ratio => 0).behind_schedule?
1350 end
1350 end
1351
1351
1352 should "be false if the issue has no end_date" do
1352 should "be false if the issue has no end_date" do
1353 assert !Issue.new(:start_date => 1.day.from_now.to_date,
1353 assert !Issue.new(:start_date => 1.day.from_now.to_date,
1354 :due_date => nil,
1354 :due_date => nil,
1355 :done_ratio => 0).behind_schedule?
1355 :done_ratio => 0).behind_schedule?
1356 end
1356 end
1357
1357
1358 should "be false if the issue has more done than it's calendar time" do
1358 should "be false if the issue has more done than it's calendar time" do
1359 assert !Issue.new(:start_date => 50.days.ago.to_date,
1359 assert !Issue.new(:start_date => 50.days.ago.to_date,
1360 :due_date => 50.days.from_now.to_date,
1360 :due_date => 50.days.from_now.to_date,
1361 :done_ratio => 90).behind_schedule?
1361 :done_ratio => 90).behind_schedule?
1362 end
1362 end
1363
1363
1364 should "be true if the issue hasn't been started at all" do
1364 should "be true if the issue hasn't been started at all" do
1365 assert Issue.new(:start_date => 1.day.ago.to_date,
1365 assert Issue.new(:start_date => 1.day.ago.to_date,
1366 :due_date => 1.day.from_now.to_date,
1366 :due_date => 1.day.from_now.to_date,
1367 :done_ratio => 0).behind_schedule?
1367 :done_ratio => 0).behind_schedule?
1368 end
1368 end
1369
1369
1370 should "be true if the issue has used more calendar time than it's done ratio" do
1370 should "be true if the issue has used more calendar time than it's done ratio" do
1371 assert Issue.new(:start_date => 100.days.ago.to_date,
1371 assert Issue.new(:start_date => 100.days.ago.to_date,
1372 :due_date => Date.today,
1372 :due_date => Date.today,
1373 :done_ratio => 90).behind_schedule?
1373 :done_ratio => 90).behind_schedule?
1374 end
1374 end
1375 end
1375 end
1376
1376
1377 context "#assignable_users" do
1377 context "#assignable_users" do
1378 should "be Users" do
1378 should "be Users" do
1379 assert_kind_of User, Issue.find(1).assignable_users.first
1379 assert_kind_of User, Issue.find(1).assignable_users.first
1380 end
1380 end
1381
1381
1382 should "include the issue author" do
1382 should "include the issue author" do
1383 non_project_member = User.generate!
1383 non_project_member = User.generate!
1384 issue = Issue.generate!(:author => non_project_member)
1384 issue = Issue.generate!(:author => non_project_member)
1385
1385
1386 assert issue.assignable_users.include?(non_project_member)
1386 assert issue.assignable_users.include?(non_project_member)
1387 end
1387 end
1388
1388
1389 should "include the current assignee" do
1389 should "include the current assignee" do
1390 user = User.generate!
1390 user = User.generate!
1391 issue = Issue.generate!(:assigned_to => user)
1391 issue = Issue.generate!(:assigned_to => user)
1392 user.lock!
1392 user.lock!
1393
1393
1394 assert Issue.find(issue.id).assignable_users.include?(user)
1394 assert Issue.find(issue.id).assignable_users.include?(user)
1395 end
1395 end
1396
1396
1397 should "not show the issue author twice" do
1397 should "not show the issue author twice" do
1398 assignable_user_ids = Issue.find(1).assignable_users.collect(&:id)
1398 assignable_user_ids = Issue.find(1).assignable_users.collect(&:id)
1399 assert_equal 2, assignable_user_ids.length
1399 assert_equal 2, assignable_user_ids.length
1400
1400
1401 assignable_user_ids.each do |user_id|
1401 assignable_user_ids.each do |user_id|
1402 assert_equal 1, assignable_user_ids.select {|i| i == user_id}.length,
1402 assert_equal 1, assignable_user_ids.select {|i| i == user_id}.length,
1403 "User #{user_id} appears more or less than once"
1403 "User #{user_id} appears more or less than once"
1404 end
1404 end
1405 end
1405 end
1406
1406
1407 context "with issue_group_assignment" do
1407 context "with issue_group_assignment" do
1408 should "include groups" do
1408 should "include groups" do
1409 issue = Issue.new(:project => Project.find(2))
1409 issue = Issue.new(:project => Project.find(2))
1410
1410
1411 with_settings :issue_group_assignment => '1' do
1411 with_settings :issue_group_assignment => '1' do
1412 assert_equal %w(Group User), issue.assignable_users.map {|a| a.class.name}.uniq.sort
1412 assert_equal %w(Group User), issue.assignable_users.map {|a| a.class.name}.uniq.sort
1413 assert issue.assignable_users.include?(Group.find(11))
1413 assert issue.assignable_users.include?(Group.find(11))
1414 end
1414 end
1415 end
1415 end
1416 end
1416 end
1417
1417
1418 context "without issue_group_assignment" do
1418 context "without issue_group_assignment" do
1419 should "not include groups" do
1419 should "not include groups" do
1420 issue = Issue.new(:project => Project.find(2))
1420 issue = Issue.new(:project => Project.find(2))
1421
1421
1422 with_settings :issue_group_assignment => '0' do
1422 with_settings :issue_group_assignment => '0' do
1423 assert_equal %w(User), issue.assignable_users.map {|a| a.class.name}.uniq.sort
1423 assert_equal %w(User), issue.assignable_users.map {|a| a.class.name}.uniq.sort
1424 assert !issue.assignable_users.include?(Group.find(11))
1424 assert !issue.assignable_users.include?(Group.find(11))
1425 end
1425 end
1426 end
1426 end
1427 end
1427 end
1428 end
1428 end
1429
1429
1430 def test_create_should_send_email_notification
1430 def test_create_should_send_email_notification
1431 ActionMailer::Base.deliveries.clear
1431 ActionMailer::Base.deliveries.clear
1432 issue = Issue.new(:project_id => 1, :tracker_id => 1,
1432 issue = Issue.new(:project_id => 1, :tracker_id => 1,
1433 :author_id => 3, :status_id => 1,
1433 :author_id => 3, :status_id => 1,
1434 :priority => IssuePriority.all.first,
1434 :priority => IssuePriority.all.first,
1435 :subject => 'test_create', :estimated_hours => '1:30')
1435 :subject => 'test_create', :estimated_hours => '1:30')
1436
1436
1437 assert issue.save
1437 assert issue.save
1438 assert_equal 1, ActionMailer::Base.deliveries.size
1438 assert_equal 1, ActionMailer::Base.deliveries.size
1439 end
1439 end
1440
1440
1441 def test_stale_issue_should_not_send_email_notification
1441 def test_stale_issue_should_not_send_email_notification
1442 ActionMailer::Base.deliveries.clear
1442 ActionMailer::Base.deliveries.clear
1443 issue = Issue.find(1)
1443 issue = Issue.find(1)
1444 stale = Issue.find(1)
1444 stale = Issue.find(1)
1445
1445
1446 issue.init_journal(User.find(1))
1446 issue.init_journal(User.find(1))
1447 issue.subject = 'Subjet update'
1447 issue.subject = 'Subjet update'
1448 assert issue.save
1448 assert issue.save
1449 assert_equal 1, ActionMailer::Base.deliveries.size
1449 assert_equal 1, ActionMailer::Base.deliveries.size
1450 ActionMailer::Base.deliveries.clear
1450 ActionMailer::Base.deliveries.clear
1451
1451
1452 stale.init_journal(User.find(1))
1452 stale.init_journal(User.find(1))
1453 stale.subject = 'Another subjet update'
1453 stale.subject = 'Another subjet update'
1454 assert_raise ActiveRecord::StaleObjectError do
1454 assert_raise ActiveRecord::StaleObjectError do
1455 stale.save
1455 stale.save
1456 end
1456 end
1457 assert ActionMailer::Base.deliveries.empty?
1457 assert ActionMailer::Base.deliveries.empty?
1458 end
1458 end
1459
1459
1460 def test_journalized_description
1460 def test_journalized_description
1461 IssueCustomField.delete_all
1461 IssueCustomField.delete_all
1462
1462
1463 i = Issue.first
1463 i = Issue.first
1464 old_description = i.description
1464 old_description = i.description
1465 new_description = "This is the new description"
1465 new_description = "This is the new description"
1466
1466
1467 i.init_journal(User.find(2))
1467 i.init_journal(User.find(2))
1468 i.description = new_description
1468 i.description = new_description
1469 assert_difference 'Journal.count', 1 do
1469 assert_difference 'Journal.count', 1 do
1470 assert_difference 'JournalDetail.count', 1 do
1470 assert_difference 'JournalDetail.count', 1 do
1471 i.save!
1471 i.save!
1472 end
1472 end
1473 end
1473 end
1474
1474
1475 detail = JournalDetail.first(:order => 'id DESC')
1475 detail = JournalDetail.first(:order => 'id DESC')
1476 assert_equal i, detail.journal.journalized
1476 assert_equal i, detail.journal.journalized
1477 assert_equal 'attr', detail.property
1477 assert_equal 'attr', detail.property
1478 assert_equal 'description', detail.prop_key
1478 assert_equal 'description', detail.prop_key
1479 assert_equal old_description, detail.old_value
1479 assert_equal old_description, detail.old_value
1480 assert_equal new_description, detail.value
1480 assert_equal new_description, detail.value
1481 end
1481 end
1482
1482
1483 def test_blank_descriptions_should_not_be_journalized
1483 def test_blank_descriptions_should_not_be_journalized
1484 IssueCustomField.delete_all
1484 IssueCustomField.delete_all
1485 Issue.update_all("description = NULL", "id=1")
1485 Issue.update_all("description = NULL", "id=1")
1486
1486
1487 i = Issue.find(1)
1487 i = Issue.find(1)
1488 i.init_journal(User.find(2))
1488 i.init_journal(User.find(2))
1489 i.subject = "blank description"
1489 i.subject = "blank description"
1490 i.description = "\r\n"
1490 i.description = "\r\n"
1491
1491
1492 assert_difference 'Journal.count', 1 do
1492 assert_difference 'Journal.count', 1 do
1493 assert_difference 'JournalDetail.count', 1 do
1493 assert_difference 'JournalDetail.count', 1 do
1494 i.save!
1494 i.save!
1495 end
1495 end
1496 end
1496 end
1497 end
1497 end
1498
1498
1499 def test_journalized_multi_custom_field
1499 def test_journalized_multi_custom_field
1500 field = IssueCustomField.create!(:name => 'filter', :field_format => 'list',
1500 field = IssueCustomField.create!(:name => 'filter', :field_format => 'list',
1501 :is_filter => true, :is_for_all => true,
1501 :is_filter => true, :is_for_all => true,
1502 :tracker_ids => [1],
1502 :tracker_ids => [1],
1503 :possible_values => ['value1', 'value2', 'value3'],
1503 :possible_values => ['value1', 'value2', 'value3'],
1504 :multiple => true)
1504 :multiple => true)
1505
1505
1506 issue = Issue.create!(:project_id => 1, :tracker_id => 1,
1506 issue = Issue.create!(:project_id => 1, :tracker_id => 1,
1507 :subject => 'Test', :author_id => 1)
1507 :subject => 'Test', :author_id => 1)
1508
1508
1509 assert_difference 'Journal.count' do
1509 assert_difference 'Journal.count' do
1510 assert_difference 'JournalDetail.count' do
1510 assert_difference 'JournalDetail.count' do
1511 issue.init_journal(User.first)
1511 issue.init_journal(User.first)
1512 issue.custom_field_values = {field.id => ['value1']}
1512 issue.custom_field_values = {field.id => ['value1']}
1513 issue.save!
1513 issue.save!
1514 end
1514 end
1515 assert_difference 'JournalDetail.count' do
1515 assert_difference 'JournalDetail.count' do
1516 issue.init_journal(User.first)
1516 issue.init_journal(User.first)
1517 issue.custom_field_values = {field.id => ['value1', 'value2']}
1517 issue.custom_field_values = {field.id => ['value1', 'value2']}
1518 issue.save!
1518 issue.save!
1519 end
1519 end
1520 assert_difference 'JournalDetail.count', 2 do
1520 assert_difference 'JournalDetail.count', 2 do
1521 issue.init_journal(User.first)
1521 issue.init_journal(User.first)
1522 issue.custom_field_values = {field.id => ['value3', 'value2']}
1522 issue.custom_field_values = {field.id => ['value3', 'value2']}
1523 issue.save!
1523 issue.save!
1524 end
1524 end
1525 assert_difference 'JournalDetail.count', 2 do
1525 assert_difference 'JournalDetail.count', 2 do
1526 issue.init_journal(User.first)
1526 issue.init_journal(User.first)
1527 issue.custom_field_values = {field.id => nil}
1527 issue.custom_field_values = {field.id => nil}
1528 issue.save!
1528 issue.save!
1529 end
1529 end
1530 end
1530 end
1531 end
1531 end
1532
1532
1533 def test_description_eol_should_be_normalized
1533 def test_description_eol_should_be_normalized
1534 i = Issue.new(:description => "CR \r LF \n CRLF \r\n")
1534 i = Issue.new(:description => "CR \r LF \n CRLF \r\n")
1535 assert_equal "CR \r\n LF \r\n CRLF \r\n", i.description
1535 assert_equal "CR \r\n LF \r\n CRLF \r\n", i.description
1536 end
1536 end
1537
1537
1538 def test_saving_twice_should_not_duplicate_journal_details
1538 def test_saving_twice_should_not_duplicate_journal_details
1539 i = Issue.find(:first)
1539 i = Issue.find(:first)
1540 i.init_journal(User.find(2), 'Some notes')
1540 i.init_journal(User.find(2), 'Some notes')
1541 # initial changes
1541 # initial changes
1542 i.subject = 'New subject'
1542 i.subject = 'New subject'
1543 i.done_ratio = i.done_ratio + 10
1543 i.done_ratio = i.done_ratio + 10
1544 assert_difference 'Journal.count' do
1544 assert_difference 'Journal.count' do
1545 assert i.save
1545 assert i.save
1546 end
1546 end
1547 # 1 more change
1547 # 1 more change
1548 i.priority = IssuePriority.find(:first, :conditions => ["id <> ?", i.priority_id])
1548 i.priority = IssuePriority.find(:first, :conditions => ["id <> ?", i.priority_id])
1549 assert_no_difference 'Journal.count' do
1549 assert_no_difference 'Journal.count' do
1550 assert_difference 'JournalDetail.count', 1 do
1550 assert_difference 'JournalDetail.count', 1 do
1551 i.save
1551 i.save
1552 end
1552 end
1553 end
1553 end
1554 # no more change
1554 # no more change
1555 assert_no_difference 'Journal.count' do
1555 assert_no_difference 'Journal.count' do
1556 assert_no_difference 'JournalDetail.count' do
1556 assert_no_difference 'JournalDetail.count' do
1557 i.save
1557 i.save
1558 end
1558 end
1559 end
1559 end
1560 end
1560 end
1561
1561
1562 def test_all_dependent_issues
1562 def test_all_dependent_issues
1563 IssueRelation.delete_all
1563 IssueRelation.delete_all
1564 assert IssueRelation.create!(:issue_from => Issue.find(1),
1564 assert IssueRelation.create!(:issue_from => Issue.find(1),
1565 :issue_to => Issue.find(2),
1565 :issue_to => Issue.find(2),
1566 :relation_type => IssueRelation::TYPE_PRECEDES)
1566 :relation_type => IssueRelation::TYPE_PRECEDES)
1567 assert IssueRelation.create!(:issue_from => Issue.find(2),
1567 assert IssueRelation.create!(:issue_from => Issue.find(2),
1568 :issue_to => Issue.find(3),
1568 :issue_to => Issue.find(3),
1569 :relation_type => IssueRelation::TYPE_PRECEDES)
1569 :relation_type => IssueRelation::TYPE_PRECEDES)
1570 assert IssueRelation.create!(:issue_from => Issue.find(3),
1570 assert IssueRelation.create!(:issue_from => Issue.find(3),
1571 :issue_to => Issue.find(8),
1571 :issue_to => Issue.find(8),
1572 :relation_type => IssueRelation::TYPE_PRECEDES)
1572 :relation_type => IssueRelation::TYPE_PRECEDES)
1573
1573
1574 assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort
1574 assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort
1575 end
1575 end
1576
1576
1577 def test_all_dependent_issues_with_persistent_circular_dependency
1577 def test_all_dependent_issues_with_persistent_circular_dependency
1578 IssueRelation.delete_all
1578 IssueRelation.delete_all
1579 assert IssueRelation.create!(:issue_from => Issue.find(1),
1579 assert IssueRelation.create!(:issue_from => Issue.find(1),
1580 :issue_to => Issue.find(2),
1580 :issue_to => Issue.find(2),
1581 :relation_type => IssueRelation::TYPE_PRECEDES)
1581 :relation_type => IssueRelation::TYPE_PRECEDES)
1582 assert IssueRelation.create!(:issue_from => Issue.find(2),
1582 assert IssueRelation.create!(:issue_from => Issue.find(2),
1583 :issue_to => Issue.find(3),
1583 :issue_to => Issue.find(3),
1584 :relation_type => IssueRelation::TYPE_PRECEDES)
1584 :relation_type => IssueRelation::TYPE_PRECEDES)
1585
1585
1586 r = IssueRelation.create!(:issue_from => Issue.find(3),
1586 r = IssueRelation.create!(:issue_from => Issue.find(3),
1587 :issue_to => Issue.find(7),
1587 :issue_to => Issue.find(7),
1588 :relation_type => IssueRelation::TYPE_PRECEDES)
1588 :relation_type => IssueRelation::TYPE_PRECEDES)
1589 IssueRelation.update_all("issue_to_id = 1", ["id = ?", r.id])
1589 IssueRelation.update_all("issue_to_id = 1", ["id = ?", r.id])
1590
1590
1591 assert_equal [2, 3], Issue.find(1).all_dependent_issues.collect(&:id).sort
1591 assert_equal [2, 3], Issue.find(1).all_dependent_issues.collect(&:id).sort
1592 end
1592 end
1593
1593
1594 def test_all_dependent_issues_with_persistent_multiple_circular_dependencies
1594 def test_all_dependent_issues_with_persistent_multiple_circular_dependencies
1595 IssueRelation.delete_all
1595 IssueRelation.delete_all
1596 assert IssueRelation.create!(:issue_from => Issue.find(1),
1596 assert IssueRelation.create!(:issue_from => Issue.find(1),
1597 :issue_to => Issue.find(2),
1597 :issue_to => Issue.find(2),
1598 :relation_type => IssueRelation::TYPE_RELATES)
1598 :relation_type => IssueRelation::TYPE_RELATES)
1599 assert IssueRelation.create!(:issue_from => Issue.find(2),
1599 assert IssueRelation.create!(:issue_from => Issue.find(2),
1600 :issue_to => Issue.find(3),
1600 :issue_to => Issue.find(3),
1601 :relation_type => IssueRelation::TYPE_RELATES)
1601 :relation_type => IssueRelation::TYPE_RELATES)
1602 assert IssueRelation.create!(:issue_from => Issue.find(3),
1602 assert IssueRelation.create!(:issue_from => Issue.find(3),
1603 :issue_to => Issue.find(8),
1603 :issue_to => Issue.find(8),
1604 :relation_type => IssueRelation::TYPE_RELATES)
1604 :relation_type => IssueRelation::TYPE_RELATES)
1605
1605
1606 r = IssueRelation.create!(:issue_from => Issue.find(8),
1606 r = IssueRelation.create!(:issue_from => Issue.find(8),
1607 :issue_to => Issue.find(7),
1607 :issue_to => Issue.find(7),
1608 :relation_type => IssueRelation::TYPE_RELATES)
1608 :relation_type => IssueRelation::TYPE_RELATES)
1609 IssueRelation.update_all("issue_to_id = 2", ["id = ?", r.id])
1609 IssueRelation.update_all("issue_to_id = 2", ["id = ?", r.id])
1610
1610
1611 r = IssueRelation.create!(:issue_from => Issue.find(3),
1611 r = IssueRelation.create!(:issue_from => Issue.find(3),
1612 :issue_to => Issue.find(7),
1612 :issue_to => Issue.find(7),
1613 :relation_type => IssueRelation::TYPE_RELATES)
1613 :relation_type => IssueRelation::TYPE_RELATES)
1614 IssueRelation.update_all("issue_to_id = 1", ["id = ?", r.id])
1614 IssueRelation.update_all("issue_to_id = 1", ["id = ?", r.id])
1615
1615
1616 assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort
1616 assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort
1617 end
1617 end
1618
1618
1619 context "#done_ratio" do
1619 context "#done_ratio" do
1620 setup do
1620 setup do
1621 @issue = Issue.find(1)
1621 @issue = Issue.find(1)
1622 @issue_status = IssueStatus.find(1)
1622 @issue_status = IssueStatus.find(1)
1623 @issue_status.update_attribute(:default_done_ratio, 50)
1623 @issue_status.update_attribute(:default_done_ratio, 50)
1624 @issue2 = Issue.find(2)
1624 @issue2 = Issue.find(2)
1625 @issue_status2 = IssueStatus.find(2)
1625 @issue_status2 = IssueStatus.find(2)
1626 @issue_status2.update_attribute(:default_done_ratio, 0)
1626 @issue_status2.update_attribute(:default_done_ratio, 0)
1627 end
1627 end
1628
1628
1629 teardown do
1629 teardown do
1630 Setting.issue_done_ratio = 'issue_field'
1630 Setting.issue_done_ratio = 'issue_field'
1631 end
1631 end
1632
1632
1633 context "with Setting.issue_done_ratio using the issue_field" do
1633 context "with Setting.issue_done_ratio using the issue_field" do
1634 setup do
1634 setup do
1635 Setting.issue_done_ratio = 'issue_field'
1635 Setting.issue_done_ratio = 'issue_field'
1636 end
1636 end
1637
1637
1638 should "read the issue's field" do
1638 should "read the issue's field" do
1639 assert_equal 0, @issue.done_ratio
1639 assert_equal 0, @issue.done_ratio
1640 assert_equal 30, @issue2.done_ratio
1640 assert_equal 30, @issue2.done_ratio
1641 end
1641 end
1642 end
1642 end
1643
1643
1644 context "with Setting.issue_done_ratio using the issue_status" do
1644 context "with Setting.issue_done_ratio using the issue_status" do
1645 setup do
1645 setup do
1646 Setting.issue_done_ratio = 'issue_status'
1646 Setting.issue_done_ratio = 'issue_status'
1647 end
1647 end
1648
1648
1649 should "read the Issue Status's default done ratio" do
1649 should "read the Issue Status's default done ratio" do
1650 assert_equal 50, @issue.done_ratio
1650 assert_equal 50, @issue.done_ratio
1651 assert_equal 0, @issue2.done_ratio
1651 assert_equal 0, @issue2.done_ratio
1652 end
1652 end
1653 end
1653 end
1654 end
1654 end
1655
1655
1656 context "#update_done_ratio_from_issue_status" do
1656 context "#update_done_ratio_from_issue_status" do
1657 setup do
1657 setup do
1658 @issue = Issue.find(1)
1658 @issue = Issue.find(1)
1659 @issue_status = IssueStatus.find(1)
1659 @issue_status = IssueStatus.find(1)
1660 @issue_status.update_attribute(:default_done_ratio, 50)
1660 @issue_status.update_attribute(:default_done_ratio, 50)
1661 @issue2 = Issue.find(2)
1661 @issue2 = Issue.find(2)
1662 @issue_status2 = IssueStatus.find(2)
1662 @issue_status2 = IssueStatus.find(2)
1663 @issue_status2.update_attribute(:default_done_ratio, 0)
1663 @issue_status2.update_attribute(:default_done_ratio, 0)
1664 end
1664 end
1665
1665
1666 context "with Setting.issue_done_ratio using the issue_field" do
1666 context "with Setting.issue_done_ratio using the issue_field" do
1667 setup do
1667 setup do
1668 Setting.issue_done_ratio = 'issue_field'
1668 Setting.issue_done_ratio = 'issue_field'
1669 end
1669 end
1670
1670
1671 should "not change the issue" do
1671 should "not change the issue" do
1672 @issue.update_done_ratio_from_issue_status
1672 @issue.update_done_ratio_from_issue_status
1673 @issue2.update_done_ratio_from_issue_status
1673 @issue2.update_done_ratio_from_issue_status
1674
1674
1675 assert_equal 0, @issue.read_attribute(:done_ratio)
1675 assert_equal 0, @issue.read_attribute(:done_ratio)
1676 assert_equal 30, @issue2.read_attribute(:done_ratio)
1676 assert_equal 30, @issue2.read_attribute(:done_ratio)
1677 end
1677 end
1678 end
1678 end
1679
1679
1680 context "with Setting.issue_done_ratio using the issue_status" do
1680 context "with Setting.issue_done_ratio using the issue_status" do
1681 setup do
1681 setup do
1682 Setting.issue_done_ratio = 'issue_status'
1682 Setting.issue_done_ratio = 'issue_status'
1683 end
1683 end
1684
1684
1685 should "change the issue's done ratio" do
1685 should "change the issue's done ratio" do
1686 @issue.update_done_ratio_from_issue_status
1686 @issue.update_done_ratio_from_issue_status
1687 @issue2.update_done_ratio_from_issue_status
1687 @issue2.update_done_ratio_from_issue_status
1688
1688
1689 assert_equal 50, @issue.read_attribute(:done_ratio)
1689 assert_equal 50, @issue.read_attribute(:done_ratio)
1690 assert_equal 0, @issue2.read_attribute(:done_ratio)
1690 assert_equal 0, @issue2.read_attribute(:done_ratio)
1691 end
1691 end
1692 end
1692 end
1693 end
1693 end
1694
1694
1695 test "#by_tracker" do
1695 test "#by_tracker" do
1696 User.current = User.anonymous
1696 User.current = User.anonymous
1697 groups = Issue.by_tracker(Project.find(1))
1697 groups = Issue.by_tracker(Project.find(1))
1698 assert_equal 3, groups.size
1698 assert_equal 3, groups.size
1699 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1699 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1700 end
1700 end
1701
1701
1702 test "#by_version" do
1702 test "#by_version" do
1703 User.current = User.anonymous
1703 User.current = User.anonymous
1704 groups = Issue.by_version(Project.find(1))
1704 groups = Issue.by_version(Project.find(1))
1705 assert_equal 3, groups.size
1705 assert_equal 3, groups.size
1706 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1706 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1707 end
1707 end
1708
1708
1709 test "#by_priority" do
1709 test "#by_priority" do
1710 User.current = User.anonymous
1710 User.current = User.anonymous
1711 groups = Issue.by_priority(Project.find(1))
1711 groups = Issue.by_priority(Project.find(1))
1712 assert_equal 4, groups.size
1712 assert_equal 4, groups.size
1713 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1713 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1714 end
1714 end
1715
1715
1716 test "#by_category" do
1716 test "#by_category" do
1717 User.current = User.anonymous
1717 User.current = User.anonymous
1718 groups = Issue.by_category(Project.find(1))
1718 groups = Issue.by_category(Project.find(1))
1719 assert_equal 2, groups.size
1719 assert_equal 2, groups.size
1720 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1720 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1721 end
1721 end
1722
1722
1723 test "#by_assigned_to" do
1723 test "#by_assigned_to" do
1724 User.current = User.anonymous
1724 User.current = User.anonymous
1725 groups = Issue.by_assigned_to(Project.find(1))
1725 groups = Issue.by_assigned_to(Project.find(1))
1726 assert_equal 2, groups.size
1726 assert_equal 2, groups.size
1727 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1727 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1728 end
1728 end
1729
1729
1730 test "#by_author" do
1730 test "#by_author" do
1731 User.current = User.anonymous
1731 User.current = User.anonymous
1732 groups = Issue.by_author(Project.find(1))
1732 groups = Issue.by_author(Project.find(1))
1733 assert_equal 4, groups.size
1733 assert_equal 4, groups.size
1734 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1734 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1735 end
1735 end
1736
1736
1737 test "#by_subproject" do
1737 test "#by_subproject" do
1738 User.current = User.anonymous
1738 User.current = User.anonymous
1739 groups = Issue.by_subproject(Project.find(1))
1739 groups = Issue.by_subproject(Project.find(1))
1740 # Private descendant not visible
1740 # Private descendant not visible
1741 assert_equal 1, groups.size
1741 assert_equal 1, groups.size
1742 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1742 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1743 end
1743 end
1744
1744
1745 def test_recently_updated_scope
1745 def test_recently_updated_scope
1746 #should return the last updated issue
1746 #should return the last updated issue
1747 assert_equal Issue.reorder("updated_on DESC").first, Issue.recently_updated.limit(1).first
1747 assert_equal Issue.reorder("updated_on DESC").first, Issue.recently_updated.limit(1).first
1748 end
1748 end
1749
1749
1750 def test_on_active_projects_scope
1750 def test_on_active_projects_scope
1751 assert Project.find(2).archive
1751 assert Project.find(2).archive
1752
1752
1753 before = Issue.on_active_project.length
1753 before = Issue.on_active_project.length
1754 # test inclusion to results
1754 # test inclusion to results
1755 issue = Issue.generate!(:tracker => Project.find(2).trackers.first)
1755 issue = Issue.generate!(:tracker => Project.find(2).trackers.first)
1756 assert_equal before + 1, Issue.on_active_project.length
1756 assert_equal before + 1, Issue.on_active_project.length
1757
1757
1758 # Move to an archived project
1758 # Move to an archived project
1759 issue.project = Project.find(2)
1759 issue.project = Project.find(2)
1760 assert issue.save
1760 assert issue.save
1761 assert_equal before, Issue.on_active_project.length
1761 assert_equal before, Issue.on_active_project.length
1762 end
1762 end
1763
1763
1764 context "Issue#recipients" do
1764 context "Issue#recipients" do
1765 setup do
1765 setup do
1766 @project = Project.find(1)
1766 @project = Project.find(1)
1767 @author = User.generate!
1767 @author = User.generate!
1768 @assignee = User.generate!
1768 @assignee = User.generate!
1769 @issue = Issue.generate!(:project => @project, :assigned_to => @assignee, :author => @author)
1769 @issue = Issue.generate!(:project => @project, :assigned_to => @assignee, :author => @author)
1770 end
1770 end
1771
1771
1772 should "include project recipients" do
1772 should "include project recipients" do
1773 assert @project.recipients.present?
1773 assert @project.recipients.present?
1774 @project.recipients.each do |project_recipient|
1774 @project.recipients.each do |project_recipient|
1775 assert @issue.recipients.include?(project_recipient)
1775 assert @issue.recipients.include?(project_recipient)
1776 end
1776 end
1777 end
1777 end
1778
1778
1779 should "include the author if the author is active" do
1779 should "include the author if the author is active" do
1780 assert @issue.author, "No author set for Issue"
1780 assert @issue.author, "No author set for Issue"
1781 assert @issue.recipients.include?(@issue.author.mail)
1781 assert @issue.recipients.include?(@issue.author.mail)
1782 end
1782 end
1783
1783
1784 should "include the assigned to user if the assigned to user is active" do
1784 should "include the assigned to user if the assigned to user is active" do
1785 assert @issue.assigned_to, "No assigned_to set for Issue"
1785 assert @issue.assigned_to, "No assigned_to set for Issue"
1786 assert @issue.recipients.include?(@issue.assigned_to.mail)
1786 assert @issue.recipients.include?(@issue.assigned_to.mail)
1787 end
1787 end
1788
1788
1789 should "not include users who opt out of all email" do
1789 should "not include users who opt out of all email" do
1790 @author.update_attribute(:mail_notification, :none)
1790 @author.update_attribute(:mail_notification, :none)
1791
1791
1792 assert !@issue.recipients.include?(@issue.author.mail)
1792 assert !@issue.recipients.include?(@issue.author.mail)
1793 end
1793 end
1794
1794
1795 should "not include the issue author if they are only notified of assigned issues" do
1795 should "not include the issue author if they are only notified of assigned issues" do
1796 @author.update_attribute(:mail_notification, :only_assigned)
1796 @author.update_attribute(:mail_notification, :only_assigned)
1797
1797
1798 assert !@issue.recipients.include?(@issue.author.mail)
1798 assert !@issue.recipients.include?(@issue.author.mail)
1799 end
1799 end
1800
1800
1801 should "not include the assigned user if they are only notified of owned issues" do
1801 should "not include the assigned user if they are only notified of owned issues" do
1802 @assignee.update_attribute(:mail_notification, :only_owner)
1802 @assignee.update_attribute(:mail_notification, :only_owner)
1803
1803
1804 assert !@issue.recipients.include?(@issue.assigned_to.mail)
1804 assert !@issue.recipients.include?(@issue.assigned_to.mail)
1805 end
1805 end
1806 end
1806 end
1807
1807
1808 def test_last_journal_id_with_journals_should_return_the_journal_id
1808 def test_last_journal_id_with_journals_should_return_the_journal_id
1809 assert_equal 2, Issue.find(1).last_journal_id
1809 assert_equal 2, Issue.find(1).last_journal_id
1810 end
1810 end
1811
1811
1812 def test_last_journal_id_without_journals_should_return_nil
1812 def test_last_journal_id_without_journals_should_return_nil
1813 assert_nil Issue.find(3).last_journal_id
1813 assert_nil Issue.find(3).last_journal_id
1814 end
1814 end
1815
1815
1816 def test_journals_after_should_return_journals_with_greater_id
1816 def test_journals_after_should_return_journals_with_greater_id
1817 assert_equal [Journal.find(2)], Issue.find(1).journals_after('1')
1817 assert_equal [Journal.find(2)], Issue.find(1).journals_after('1')
1818 assert_equal [], Issue.find(1).journals_after('2')
1818 assert_equal [], Issue.find(1).journals_after('2')
1819 end
1819 end
1820
1820
1821 def test_journals_after_with_blank_arg_should_return_all_journals
1821 def test_journals_after_with_blank_arg_should_return_all_journals
1822 assert_equal [Journal.find(1), Journal.find(2)], Issue.find(1).journals_after('')
1822 assert_equal [Journal.find(1), Journal.find(2)], Issue.find(1).journals_after('')
1823 end
1823 end
1824
1825 def test_css_classes_should_include_priority
1826 issue = Issue.new(:priority => IssuePriority.find(8))
1827 classes = issue.css_classes.split(' ')
1828 assert_include 'priority-8', classes
1829 assert_include 'priority-highest', classes
1830 end
1824 end
1831 end
General Comments 0
You need to be logged in to leave comments. Login now