##// END OF EJS Templates
Notify previous assignee when assignee changes (#2694)....
Jean-Philippe Lang -
r8575:22317105f99e
parent child
Show More
@@ -1,1044 +1,1052
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class Issue < ActiveRecord::Base
18 class Issue < ActiveRecord::Base
19 include Redmine::SafeAttributes
19 include Redmine::SafeAttributes
20
20
21 belongs_to :project
21 belongs_to :project
22 belongs_to :tracker
22 belongs_to :tracker
23 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
23 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
24 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
24 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
25 belongs_to :assigned_to, :class_name => 'Principal', :foreign_key => 'assigned_to_id'
25 belongs_to :assigned_to, :class_name => 'Principal', :foreign_key => 'assigned_to_id'
26 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
26 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
27 belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
27 belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
28 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
28 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
29
29
30 has_many :journals, :as => :journalized, :dependent => :destroy
30 has_many :journals, :as => :journalized, :dependent => :destroy
31 has_many :time_entries, :dependent => :delete_all
31 has_many :time_entries, :dependent => :delete_all
32 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
32 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
33
33
34 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
34 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
35 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
35 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
36
36
37 acts_as_nested_set :scope => 'root_id', :dependent => :destroy
37 acts_as_nested_set :scope => 'root_id', :dependent => :destroy
38 acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed
38 acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed
39 acts_as_customizable
39 acts_as_customizable
40 acts_as_watchable
40 acts_as_watchable
41 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
41 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
42 :include => [:project, :journals],
42 :include => [:project, :journals],
43 # sort by id so that limited eager loading doesn't break with postgresql
43 # sort by id so that limited eager loading doesn't break with postgresql
44 :order_column => "#{table_name}.id"
44 :order_column => "#{table_name}.id"
45 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
45 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
46 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
46 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
47 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
47 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
48
48
49 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
49 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
50 :author_key => :author_id
50 :author_key => :author_id
51
51
52 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
52 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
53
53
54 attr_reader :current_journal
54 attr_reader :current_journal
55
55
56 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
56 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
57
57
58 validates_length_of :subject, :maximum => 255
58 validates_length_of :subject, :maximum => 255
59 validates_inclusion_of :done_ratio, :in => 0..100
59 validates_inclusion_of :done_ratio, :in => 0..100
60 validates_numericality_of :estimated_hours, :allow_nil => true
60 validates_numericality_of :estimated_hours, :allow_nil => true
61 validate :validate_issue
61 validate :validate_issue
62
62
63 named_scope :visible, lambda {|*args| { :include => :project,
63 named_scope :visible, lambda {|*args| { :include => :project,
64 :conditions => Issue.visible_condition(args.shift || User.current, *args) } }
64 :conditions => Issue.visible_condition(args.shift || User.current, *args) } }
65
65
66 named_scope :open, lambda {|*args|
66 named_scope :open, lambda {|*args|
67 is_closed = args.size > 0 ? !args.first : false
67 is_closed = args.size > 0 ? !args.first : false
68 {:conditions => ["#{IssueStatus.table_name}.is_closed = ?", is_closed], :include => :status}
68 {:conditions => ["#{IssueStatus.table_name}.is_closed = ?", is_closed], :include => :status}
69 }
69 }
70
70
71 named_scope :recently_updated, :order => "#{Issue.table_name}.updated_on DESC"
71 named_scope :recently_updated, :order => "#{Issue.table_name}.updated_on DESC"
72 named_scope :with_limit, lambda { |limit| { :limit => limit} }
72 named_scope :with_limit, lambda { |limit| { :limit => limit} }
73 named_scope :on_active_project, :include => [:status, :project, :tracker],
73 named_scope :on_active_project, :include => [:status, :project, :tracker],
74 :conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"]
74 :conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"]
75
75
76 before_create :default_assign
76 before_create :default_assign
77 before_save :close_duplicates, :update_done_ratio_from_issue_status
77 before_save :close_duplicates, :update_done_ratio_from_issue_status
78 after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?}
78 after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?}
79 after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal
79 after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal
80 after_destroy :update_parent_attributes
80 after_destroy :update_parent_attributes
81
81
82 # Returns a SQL conditions string used to find all issues visible by the specified user
82 # Returns a SQL conditions string used to find all issues visible by the specified user
83 def self.visible_condition(user, options={})
83 def self.visible_condition(user, options={})
84 Project.allowed_to_condition(user, :view_issues, options) do |role, user|
84 Project.allowed_to_condition(user, :view_issues, options) do |role, user|
85 case role.issues_visibility
85 case role.issues_visibility
86 when 'all'
86 when 'all'
87 nil
87 nil
88 when 'default'
88 when 'default'
89 user_ids = [user.id] + user.groups.map(&:id)
89 user_ids = [user.id] + user.groups.map(&:id)
90 "(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
90 "(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
91 when 'own'
91 when 'own'
92 user_ids = [user.id] + user.groups.map(&:id)
92 user_ids = [user.id] + user.groups.map(&:id)
93 "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
93 "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
94 else
94 else
95 '1=0'
95 '1=0'
96 end
96 end
97 end
97 end
98 end
98 end
99
99
100 # Returns true if usr or current user is allowed to view the issue
100 # Returns true if usr or current user is allowed to view the issue
101 def visible?(usr=nil)
101 def visible?(usr=nil)
102 (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
102 (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
103 case role.issues_visibility
103 case role.issues_visibility
104 when 'all'
104 when 'all'
105 true
105 true
106 when 'default'
106 when 'default'
107 !self.is_private? || self.author == user || user.is_or_belongs_to?(assigned_to)
107 !self.is_private? || self.author == user || user.is_or_belongs_to?(assigned_to)
108 when 'own'
108 when 'own'
109 self.author == user || user.is_or_belongs_to?(assigned_to)
109 self.author == user || user.is_or_belongs_to?(assigned_to)
110 else
110 else
111 false
111 false
112 end
112 end
113 end
113 end
114 end
114 end
115
115
116 def initialize(attributes=nil, *args)
116 def initialize(attributes=nil, *args)
117 super
117 super
118 if new_record?
118 if new_record?
119 # set default values for new records only
119 # set default values for new records only
120 self.status ||= IssueStatus.default
120 self.status ||= IssueStatus.default
121 self.priority ||= IssuePriority.default
121 self.priority ||= IssuePriority.default
122 self.watcher_user_ids = []
122 self.watcher_user_ids = []
123 end
123 end
124 end
124 end
125
125
126 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
126 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
127 def available_custom_fields
127 def available_custom_fields
128 (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields.all) : []
128 (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields.all) : []
129 end
129 end
130
130
131 # Copies attributes from another issue, arg can be an id or an Issue
131 # Copies attributes from another issue, arg can be an id or an Issue
132 def copy_from(arg, options={})
132 def copy_from(arg, options={})
133 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
133 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
134 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
134 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
135 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
135 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
136 self.status = issue.status
136 self.status = issue.status
137 self.author = User.current
137 self.author = User.current
138 unless options[:attachments] == false
138 unless options[:attachments] == false
139 self.attachments = issue.attachments.map do |attachement|
139 self.attachments = issue.attachments.map do |attachement|
140 attachement.copy(:container => self)
140 attachement.copy(:container => self)
141 end
141 end
142 end
142 end
143 @copied_from = issue
143 @copied_from = issue
144 self
144 self
145 end
145 end
146
146
147 # Returns an unsaved copy of the issue
147 # Returns an unsaved copy of the issue
148 def copy(attributes=nil)
148 def copy(attributes=nil)
149 copy = self.class.new.copy_from(self)
149 copy = self.class.new.copy_from(self)
150 copy.attributes = attributes if attributes
150 copy.attributes = attributes if attributes
151 copy
151 copy
152 end
152 end
153
153
154 # Returns true if the issue is a copy
154 # Returns true if the issue is a copy
155 def copy?
155 def copy?
156 @copied_from.present?
156 @copied_from.present?
157 end
157 end
158
158
159 # Moves/copies an issue to a new project and tracker
159 # Moves/copies an issue to a new project and tracker
160 # Returns the moved/copied issue on success, false on failure
160 # Returns the moved/copied issue on success, false on failure
161 def move_to_project(new_project, new_tracker=nil, options={})
161 def move_to_project(new_project, new_tracker=nil, options={})
162 ActiveSupport::Deprecation.warn "Issue#move_to_project is deprecated, use #project= instead."
162 ActiveSupport::Deprecation.warn "Issue#move_to_project is deprecated, use #project= instead."
163
163
164 if options[:copy]
164 if options[:copy]
165 issue = self.copy
165 issue = self.copy
166 else
166 else
167 issue = self
167 issue = self
168 end
168 end
169
169
170 issue.init_journal(User.current, options[:notes])
170 issue.init_journal(User.current, options[:notes])
171
171
172 # Preserve previous behaviour
172 # Preserve previous behaviour
173 # #move_to_project doesn't change tracker automatically
173 # #move_to_project doesn't change tracker automatically
174 issue.send :project=, new_project, true
174 issue.send :project=, new_project, true
175 if new_tracker
175 if new_tracker
176 issue.tracker = new_tracker
176 issue.tracker = new_tracker
177 end
177 end
178 # Allow bulk setting of attributes on the issue
178 # Allow bulk setting of attributes on the issue
179 if options[:attributes]
179 if options[:attributes]
180 issue.attributes = options[:attributes]
180 issue.attributes = options[:attributes]
181 end
181 end
182
182
183 issue.save ? issue : false
183 issue.save ? issue : false
184 end
184 end
185
185
186 def status_id=(sid)
186 def status_id=(sid)
187 self.status = nil
187 self.status = nil
188 write_attribute(:status_id, sid)
188 write_attribute(:status_id, sid)
189 end
189 end
190
190
191 def priority_id=(pid)
191 def priority_id=(pid)
192 self.priority = nil
192 self.priority = nil
193 write_attribute(:priority_id, pid)
193 write_attribute(:priority_id, pid)
194 end
194 end
195
195
196 def category_id=(cid)
196 def category_id=(cid)
197 self.category = nil
197 self.category = nil
198 write_attribute(:category_id, cid)
198 write_attribute(:category_id, cid)
199 end
199 end
200
200
201 def fixed_version_id=(vid)
201 def fixed_version_id=(vid)
202 self.fixed_version = nil
202 self.fixed_version = nil
203 write_attribute(:fixed_version_id, vid)
203 write_attribute(:fixed_version_id, vid)
204 end
204 end
205
205
206 def tracker_id=(tid)
206 def tracker_id=(tid)
207 self.tracker = nil
207 self.tracker = nil
208 result = write_attribute(:tracker_id, tid)
208 result = write_attribute(:tracker_id, tid)
209 @custom_field_values = nil
209 @custom_field_values = nil
210 result
210 result
211 end
211 end
212
212
213 def project_id=(project_id)
213 def project_id=(project_id)
214 if project_id.to_s != self.project_id.to_s
214 if project_id.to_s != self.project_id.to_s
215 self.project = (project_id.present? ? Project.find_by_id(project_id) : nil)
215 self.project = (project_id.present? ? Project.find_by_id(project_id) : nil)
216 end
216 end
217 end
217 end
218
218
219 def project=(project, keep_tracker=false)
219 def project=(project, keep_tracker=false)
220 project_was = self.project
220 project_was = self.project
221 write_attribute(:project_id, project ? project.id : nil)
221 write_attribute(:project_id, project ? project.id : nil)
222 association_instance_set('project', project)
222 association_instance_set('project', project)
223 if project_was && project && project_was != project
223 if project_was && project && project_was != project
224 unless keep_tracker || project.trackers.include?(tracker)
224 unless keep_tracker || project.trackers.include?(tracker)
225 self.tracker = project.trackers.first
225 self.tracker = project.trackers.first
226 end
226 end
227 # Reassign to the category with same name if any
227 # Reassign to the category with same name if any
228 if category
228 if category
229 self.category = project.issue_categories.find_by_name(category.name)
229 self.category = project.issue_categories.find_by_name(category.name)
230 end
230 end
231 # Keep the fixed_version if it's still valid in the new_project
231 # Keep the fixed_version if it's still valid in the new_project
232 if fixed_version && fixed_version.project != project && !project.shared_versions.include?(fixed_version)
232 if fixed_version && fixed_version.project != project && !project.shared_versions.include?(fixed_version)
233 self.fixed_version = nil
233 self.fixed_version = nil
234 end
234 end
235 if parent && parent.project_id != project_id
235 if parent && parent.project_id != project_id
236 self.parent_issue_id = nil
236 self.parent_issue_id = nil
237 end
237 end
238 @custom_field_values = nil
238 @custom_field_values = nil
239 end
239 end
240 end
240 end
241
241
242 def description=(arg)
242 def description=(arg)
243 if arg.is_a?(String)
243 if arg.is_a?(String)
244 arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n")
244 arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n")
245 end
245 end
246 write_attribute(:description, arg)
246 write_attribute(:description, arg)
247 end
247 end
248
248
249 # Overrides attributes= so that project and tracker get assigned first
249 # Overrides attributes= so that project and tracker get assigned first
250 def attributes_with_project_and_tracker_first=(new_attributes, *args)
250 def attributes_with_project_and_tracker_first=(new_attributes, *args)
251 return if new_attributes.nil?
251 return if new_attributes.nil?
252 attrs = new_attributes.dup
252 attrs = new_attributes.dup
253 attrs.stringify_keys!
253 attrs.stringify_keys!
254
254
255 %w(project project_id tracker tracker_id).each do |attr|
255 %w(project project_id tracker tracker_id).each do |attr|
256 if attrs.has_key?(attr)
256 if attrs.has_key?(attr)
257 send "#{attr}=", attrs.delete(attr)
257 send "#{attr}=", attrs.delete(attr)
258 end
258 end
259 end
259 end
260 send :attributes_without_project_and_tracker_first=, attrs, *args
260 send :attributes_without_project_and_tracker_first=, attrs, *args
261 end
261 end
262 # Do not redefine alias chain on reload (see #4838)
262 # Do not redefine alias chain on reload (see #4838)
263 alias_method_chain(:attributes=, :project_and_tracker_first) unless method_defined?(:attributes_without_project_and_tracker_first=)
263 alias_method_chain(:attributes=, :project_and_tracker_first) unless method_defined?(:attributes_without_project_and_tracker_first=)
264
264
265 def estimated_hours=(h)
265 def estimated_hours=(h)
266 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
266 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
267 end
267 end
268
268
269 safe_attributes 'project_id',
269 safe_attributes 'project_id',
270 :if => lambda {|issue, user|
270 :if => lambda {|issue, user|
271 if issue.new_record?
271 if issue.new_record?
272 issue.copy?
272 issue.copy?
273 elsif user.allowed_to?(:move_issues, issue.project)
273 elsif user.allowed_to?(:move_issues, issue.project)
274 projects = Issue.allowed_target_projects_on_move(user)
274 projects = Issue.allowed_target_projects_on_move(user)
275 projects.include?(issue.project) && projects.size > 1
275 projects.include?(issue.project) && projects.size > 1
276 end
276 end
277 }
277 }
278
278
279 safe_attributes 'tracker_id',
279 safe_attributes 'tracker_id',
280 'status_id',
280 'status_id',
281 'category_id',
281 'category_id',
282 'assigned_to_id',
282 'assigned_to_id',
283 'priority_id',
283 'priority_id',
284 'fixed_version_id',
284 'fixed_version_id',
285 'subject',
285 'subject',
286 'description',
286 'description',
287 'start_date',
287 'start_date',
288 'due_date',
288 'due_date',
289 'done_ratio',
289 'done_ratio',
290 'estimated_hours',
290 'estimated_hours',
291 'custom_field_values',
291 'custom_field_values',
292 'custom_fields',
292 'custom_fields',
293 'lock_version',
293 'lock_version',
294 :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
294 :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
295
295
296 safe_attributes 'status_id',
296 safe_attributes 'status_id',
297 'assigned_to_id',
297 'assigned_to_id',
298 'fixed_version_id',
298 'fixed_version_id',
299 'done_ratio',
299 'done_ratio',
300 'lock_version',
300 'lock_version',
301 :if => lambda {|issue, user| issue.new_statuses_allowed_to(user).any? }
301 :if => lambda {|issue, user| issue.new_statuses_allowed_to(user).any? }
302
302
303 safe_attributes 'watcher_user_ids',
303 safe_attributes 'watcher_user_ids',
304 :if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)}
304 :if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)}
305
305
306 safe_attributes 'is_private',
306 safe_attributes 'is_private',
307 :if => lambda {|issue, user|
307 :if => lambda {|issue, user|
308 user.allowed_to?(:set_issues_private, issue.project) ||
308 user.allowed_to?(:set_issues_private, issue.project) ||
309 (issue.author == user && user.allowed_to?(:set_own_issues_private, issue.project))
309 (issue.author == user && user.allowed_to?(:set_own_issues_private, issue.project))
310 }
310 }
311
311
312 safe_attributes 'parent_issue_id',
312 safe_attributes 'parent_issue_id',
313 :if => lambda {|issue, user| (issue.new_record? || user.allowed_to?(:edit_issues, issue.project)) &&
313 :if => lambda {|issue, user| (issue.new_record? || user.allowed_to?(:edit_issues, issue.project)) &&
314 user.allowed_to?(:manage_subtasks, issue.project)}
314 user.allowed_to?(:manage_subtasks, issue.project)}
315
315
316 # Safely sets attributes
316 # Safely sets attributes
317 # Should be called from controllers instead of #attributes=
317 # Should be called from controllers instead of #attributes=
318 # attr_accessible is too rough because we still want things like
318 # attr_accessible is too rough because we still want things like
319 # Issue.new(:project => foo) to work
319 # Issue.new(:project => foo) to work
320 def safe_attributes=(attrs, user=User.current)
320 def safe_attributes=(attrs, user=User.current)
321 return unless attrs.is_a?(Hash)
321 return unless attrs.is_a?(Hash)
322
322
323 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
323 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
324 attrs = delete_unsafe_attributes(attrs, user)
324 attrs = delete_unsafe_attributes(attrs, user)
325 return if attrs.empty?
325 return if attrs.empty?
326
326
327 # Project and Tracker must be set before since new_statuses_allowed_to depends on it.
327 # Project and Tracker must be set before since new_statuses_allowed_to depends on it.
328 if p = attrs.delete('project_id')
328 if p = attrs.delete('project_id')
329 if allowed_target_projects(user).collect(&:id).include?(p.to_i)
329 if allowed_target_projects(user).collect(&:id).include?(p.to_i)
330 self.project_id = p
330 self.project_id = p
331 end
331 end
332 end
332 end
333
333
334 if t = attrs.delete('tracker_id')
334 if t = attrs.delete('tracker_id')
335 self.tracker_id = t
335 self.tracker_id = t
336 end
336 end
337
337
338 if attrs['status_id']
338 if attrs['status_id']
339 unless new_statuses_allowed_to(user).collect(&:id).include?(attrs['status_id'].to_i)
339 unless new_statuses_allowed_to(user).collect(&:id).include?(attrs['status_id'].to_i)
340 attrs.delete('status_id')
340 attrs.delete('status_id')
341 end
341 end
342 end
342 end
343
343
344 unless leaf?
344 unless leaf?
345 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
345 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
346 end
346 end
347
347
348 if attrs['parent_issue_id'].present?
348 if attrs['parent_issue_id'].present?
349 attrs.delete('parent_issue_id') unless Issue.visible(user).exists?(attrs['parent_issue_id'].to_i)
349 attrs.delete('parent_issue_id') unless Issue.visible(user).exists?(attrs['parent_issue_id'].to_i)
350 end
350 end
351
351
352 # mass-assignment security bypass
352 # mass-assignment security bypass
353 self.send :attributes=, attrs, false
353 self.send :attributes=, attrs, false
354 end
354 end
355
355
356 def done_ratio
356 def done_ratio
357 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
357 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
358 status.default_done_ratio
358 status.default_done_ratio
359 else
359 else
360 read_attribute(:done_ratio)
360 read_attribute(:done_ratio)
361 end
361 end
362 end
362 end
363
363
364 def self.use_status_for_done_ratio?
364 def self.use_status_for_done_ratio?
365 Setting.issue_done_ratio == 'issue_status'
365 Setting.issue_done_ratio == 'issue_status'
366 end
366 end
367
367
368 def self.use_field_for_done_ratio?
368 def self.use_field_for_done_ratio?
369 Setting.issue_done_ratio == 'issue_field'
369 Setting.issue_done_ratio == 'issue_field'
370 end
370 end
371
371
372 def validate_issue
372 def validate_issue
373 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
373 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
374 errors.add :due_date, :not_a_date
374 errors.add :due_date, :not_a_date
375 end
375 end
376
376
377 if self.due_date and self.start_date and self.due_date < self.start_date
377 if self.due_date and self.start_date and self.due_date < self.start_date
378 errors.add :due_date, :greater_than_start_date
378 errors.add :due_date, :greater_than_start_date
379 end
379 end
380
380
381 if start_date && soonest_start && start_date < soonest_start
381 if start_date && soonest_start && start_date < soonest_start
382 errors.add :start_date, :invalid
382 errors.add :start_date, :invalid
383 end
383 end
384
384
385 if fixed_version
385 if fixed_version
386 if !assignable_versions.include?(fixed_version)
386 if !assignable_versions.include?(fixed_version)
387 errors.add :fixed_version_id, :inclusion
387 errors.add :fixed_version_id, :inclusion
388 elsif reopened? && fixed_version.closed?
388 elsif reopened? && fixed_version.closed?
389 errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version)
389 errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version)
390 end
390 end
391 end
391 end
392
392
393 # Checks that the issue can not be added/moved to a disabled tracker
393 # Checks that the issue can not be added/moved to a disabled tracker
394 if project && (tracker_id_changed? || project_id_changed?)
394 if project && (tracker_id_changed? || project_id_changed?)
395 unless project.trackers.include?(tracker)
395 unless project.trackers.include?(tracker)
396 errors.add :tracker_id, :inclusion
396 errors.add :tracker_id, :inclusion
397 end
397 end
398 end
398 end
399
399
400 # Checks parent issue assignment
400 # Checks parent issue assignment
401 if @parent_issue
401 if @parent_issue
402 if @parent_issue.project_id != project_id
402 if @parent_issue.project_id != project_id
403 errors.add :parent_issue_id, :not_same_project
403 errors.add :parent_issue_id, :not_same_project
404 elsif !new_record?
404 elsif !new_record?
405 # moving an existing issue
405 # moving an existing issue
406 if @parent_issue.root_id != root_id
406 if @parent_issue.root_id != root_id
407 # we can always move to another tree
407 # we can always move to another tree
408 elsif move_possible?(@parent_issue)
408 elsif move_possible?(@parent_issue)
409 # move accepted inside tree
409 # move accepted inside tree
410 else
410 else
411 errors.add :parent_issue_id, :not_a_valid_parent
411 errors.add :parent_issue_id, :not_a_valid_parent
412 end
412 end
413 end
413 end
414 end
414 end
415 end
415 end
416
416
417 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
417 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
418 # even if the user turns off the setting later
418 # even if the user turns off the setting later
419 def update_done_ratio_from_issue_status
419 def update_done_ratio_from_issue_status
420 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
420 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
421 self.done_ratio = status.default_done_ratio
421 self.done_ratio = status.default_done_ratio
422 end
422 end
423 end
423 end
424
424
425 def init_journal(user, notes = "")
425 def init_journal(user, notes = "")
426 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
426 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
427 if new_record?
427 if new_record?
428 @current_journal.notify = false
428 @current_journal.notify = false
429 else
429 else
430 @attributes_before_change = attributes.dup
430 @attributes_before_change = attributes.dup
431 @custom_values_before_change = {}
431 @custom_values_before_change = {}
432 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
432 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
433 end
433 end
434 # Make sure updated_on is updated when adding a note.
434 # Make sure updated_on is updated when adding a note.
435 updated_on_will_change!
435 updated_on_will_change!
436 @current_journal
436 @current_journal
437 end
437 end
438
438
439 # Return true if the issue is closed, otherwise false
439 # Return true if the issue is closed, otherwise false
440 def closed?
440 def closed?
441 self.status.is_closed?
441 self.status.is_closed?
442 end
442 end
443
443
444 # Return true if the issue is being reopened
444 # Return true if the issue is being reopened
445 def reopened?
445 def reopened?
446 if !new_record? && status_id_changed?
446 if !new_record? && status_id_changed?
447 status_was = IssueStatus.find_by_id(status_id_was)
447 status_was = IssueStatus.find_by_id(status_id_was)
448 status_new = IssueStatus.find_by_id(status_id)
448 status_new = IssueStatus.find_by_id(status_id)
449 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
449 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
450 return true
450 return true
451 end
451 end
452 end
452 end
453 false
453 false
454 end
454 end
455
455
456 # Return true if the issue is being closed
456 # Return true if the issue is being closed
457 def closing?
457 def closing?
458 if !new_record? && status_id_changed?
458 if !new_record? && status_id_changed?
459 status_was = IssueStatus.find_by_id(status_id_was)
459 status_was = IssueStatus.find_by_id(status_id_was)
460 status_new = IssueStatus.find_by_id(status_id)
460 status_new = IssueStatus.find_by_id(status_id)
461 if status_was && status_new && !status_was.is_closed? && status_new.is_closed?
461 if status_was && status_new && !status_was.is_closed? && status_new.is_closed?
462 return true
462 return true
463 end
463 end
464 end
464 end
465 false
465 false
466 end
466 end
467
467
468 # Returns true if the issue is overdue
468 # Returns true if the issue is overdue
469 def overdue?
469 def overdue?
470 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
470 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
471 end
471 end
472
472
473 # Is the amount of work done less than it should for the due date
473 # Is the amount of work done less than it should for the due date
474 def behind_schedule?
474 def behind_schedule?
475 return false if start_date.nil? || due_date.nil?
475 return false if start_date.nil? || due_date.nil?
476 done_date = start_date + ((due_date - start_date+1)* done_ratio/100).floor
476 done_date = start_date + ((due_date - start_date+1)* done_ratio/100).floor
477 return done_date <= Date.today
477 return done_date <= Date.today
478 end
478 end
479
479
480 # Does this issue have children?
480 # Does this issue have children?
481 def children?
481 def children?
482 !leaf?
482 !leaf?
483 end
483 end
484
484
485 # Users the issue can be assigned to
485 # Users the issue can be assigned to
486 def assignable_users
486 def assignable_users
487 users = project.assignable_users
487 users = project.assignable_users
488 users << author if author
488 users << author if author
489 users << assigned_to if assigned_to
489 users << assigned_to if assigned_to
490 users.uniq.sort
490 users.uniq.sort
491 end
491 end
492
492
493 # Versions that the issue can be assigned to
493 # Versions that the issue can be assigned to
494 def assignable_versions
494 def assignable_versions
495 @assignable_versions ||= (project.shared_versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort
495 @assignable_versions ||= (project.shared_versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort
496 end
496 end
497
497
498 # Returns true if this issue is blocked by another issue that is still open
498 # Returns true if this issue is blocked by another issue that is still open
499 def blocked?
499 def blocked?
500 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
500 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
501 end
501 end
502
502
503 # Returns an array of status that user is able to apply
503 # Returns an array of status that user is able to apply
504 def new_statuses_allowed_to(user, include_default=false)
504 def new_statuses_allowed_to(user, include_default=false)
505 statuses = status.find_new_statuses_allowed_to(
505 statuses = status.find_new_statuses_allowed_to(
506 user.roles_for_project(project),
506 user.roles_for_project(project),
507 tracker,
507 tracker,
508 author == user,
508 author == user,
509 assigned_to_id_changed? ? assigned_to_id_was == user.id : assigned_to_id == user.id
509 assigned_to_id_changed? ? assigned_to_id_was == user.id : assigned_to_id == user.id
510 )
510 )
511 statuses << status unless statuses.empty?
511 statuses << status unless statuses.empty?
512 statuses << IssueStatus.default if include_default
512 statuses << IssueStatus.default if include_default
513 statuses = statuses.uniq.sort
513 statuses = statuses.uniq.sort
514 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
514 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
515 end
515 end
516
516
517 def assigned_to_was
518 if assigned_to_id_changed? && assigned_to_id_was.present?
519 @assigned_to_was ||= User.find_by_id(assigned_to_id_was)
520 end
521 end
522
517 # Returns the mail adresses of users that should be notified
523 # Returns the mail adresses of users that should be notified
518 def recipients
524 def recipients
519 notified = project.notified_users
525 notified = []
520 # Author and assignee are always notified unless they have been
526 # Author and assignee are always notified unless they have been
521 # locked or don't want to be notified
527 # locked or don't want to be notified
522 notified << author if author && author.active? && author.notify_about?(self)
528 notified << author if author
523 if assigned_to
529 if assigned_to
524 if assigned_to.is_a?(Group)
530 notified += (assigned_to.is_a?(Group) ? assigned_to.users : [assigned_to])
525 notified += assigned_to.users.select {|u| u.active? && u.notify_about?(self)}
531 end
526 else
532 if assigned_to_was
527 notified << assigned_to if assigned_to.active? && assigned_to.notify_about?(self)
533 notified += (assigned_to_was.is_a?(Group) ? assigned_to_was.users : [assigned_to_was])
528 end
529 end
534 end
535 notified = notified.select {|u| u.active? && u.notify_about?(self)}
536
537 notified += project.notified_users
530 notified.uniq!
538 notified.uniq!
531 # Remove users that can not view the issue
539 # Remove users that can not view the issue
532 notified.reject! {|user| !visible?(user)}
540 notified.reject! {|user| !visible?(user)}
533 notified.collect(&:mail)
541 notified.collect(&:mail)
534 end
542 end
535
543
536 # Returns the number of hours spent on this issue
544 # Returns the number of hours spent on this issue
537 def spent_hours
545 def spent_hours
538 @spent_hours ||= time_entries.sum(:hours) || 0
546 @spent_hours ||= time_entries.sum(:hours) || 0
539 end
547 end
540
548
541 # Returns the total number of hours spent on this issue and its descendants
549 # Returns the total number of hours spent on this issue and its descendants
542 #
550 #
543 # Example:
551 # Example:
544 # spent_hours => 0.0
552 # spent_hours => 0.0
545 # spent_hours => 50.2
553 # spent_hours => 50.2
546 def total_spent_hours
554 def total_spent_hours
547 @total_spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours",
555 @total_spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours",
548 :joins => "LEFT JOIN #{TimeEntry.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id").to_f || 0.0
556 :joins => "LEFT JOIN #{TimeEntry.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id").to_f || 0.0
549 end
557 end
550
558
551 def relations
559 def relations
552 @relations ||= (relations_from + relations_to).sort
560 @relations ||= (relations_from + relations_to).sort
553 end
561 end
554
562
555 # Preloads relations for a collection of issues
563 # Preloads relations for a collection of issues
556 def self.load_relations(issues)
564 def self.load_relations(issues)
557 if issues.any?
565 if issues.any?
558 relations = IssueRelation.all(:conditions => ["issue_from_id IN (:ids) OR issue_to_id IN (:ids)", {:ids => issues.map(&:id)}])
566 relations = IssueRelation.all(:conditions => ["issue_from_id IN (:ids) OR issue_to_id IN (:ids)", {:ids => issues.map(&:id)}])
559 issues.each do |issue|
567 issues.each do |issue|
560 issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
568 issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
561 end
569 end
562 end
570 end
563 end
571 end
564
572
565 # Preloads visible spent time for a collection of issues
573 # Preloads visible spent time for a collection of issues
566 def self.load_visible_spent_hours(issues, user=User.current)
574 def self.load_visible_spent_hours(issues, user=User.current)
567 if issues.any?
575 if issues.any?
568 hours_by_issue_id = TimeEntry.visible(user).sum(:hours, :group => :issue_id)
576 hours_by_issue_id = TimeEntry.visible(user).sum(:hours, :group => :issue_id)
569 issues.each do |issue|
577 issues.each do |issue|
570 issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0)
578 issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0)
571 end
579 end
572 end
580 end
573 end
581 end
574
582
575 # Finds an issue relation given its id.
583 # Finds an issue relation given its id.
576 def find_relation(relation_id)
584 def find_relation(relation_id)
577 IssueRelation.find(relation_id, :conditions => ["issue_to_id = ? OR issue_from_id = ?", id, id])
585 IssueRelation.find(relation_id, :conditions => ["issue_to_id = ? OR issue_from_id = ?", id, id])
578 end
586 end
579
587
580 def all_dependent_issues(except=[])
588 def all_dependent_issues(except=[])
581 except << self
589 except << self
582 dependencies = []
590 dependencies = []
583 relations_from.each do |relation|
591 relations_from.each do |relation|
584 if relation.issue_to && !except.include?(relation.issue_to)
592 if relation.issue_to && !except.include?(relation.issue_to)
585 dependencies << relation.issue_to
593 dependencies << relation.issue_to
586 dependencies += relation.issue_to.all_dependent_issues(except)
594 dependencies += relation.issue_to.all_dependent_issues(except)
587 end
595 end
588 end
596 end
589 dependencies
597 dependencies
590 end
598 end
591
599
592 # Returns an array of issues that duplicate this one
600 # Returns an array of issues that duplicate this one
593 def duplicates
601 def duplicates
594 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
602 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
595 end
603 end
596
604
597 # Returns the due date or the target due date if any
605 # Returns the due date or the target due date if any
598 # Used on gantt chart
606 # Used on gantt chart
599 def due_before
607 def due_before
600 due_date || (fixed_version ? fixed_version.effective_date : nil)
608 due_date || (fixed_version ? fixed_version.effective_date : nil)
601 end
609 end
602
610
603 # Returns the time scheduled for this issue.
611 # Returns the time scheduled for this issue.
604 #
612 #
605 # Example:
613 # Example:
606 # Start Date: 2/26/09, End Date: 3/04/09
614 # Start Date: 2/26/09, End Date: 3/04/09
607 # duration => 6
615 # duration => 6
608 def duration
616 def duration
609 (start_date && due_date) ? due_date - start_date : 0
617 (start_date && due_date) ? due_date - start_date : 0
610 end
618 end
611
619
612 def soonest_start
620 def soonest_start
613 @soonest_start ||= (
621 @soonest_start ||= (
614 relations_to.collect{|relation| relation.successor_soonest_start} +
622 relations_to.collect{|relation| relation.successor_soonest_start} +
615 ancestors.collect(&:soonest_start)
623 ancestors.collect(&:soonest_start)
616 ).compact.max
624 ).compact.max
617 end
625 end
618
626
619 def reschedule_after(date)
627 def reschedule_after(date)
620 return if date.nil?
628 return if date.nil?
621 if leaf?
629 if leaf?
622 if start_date.nil? || start_date < date
630 if start_date.nil? || start_date < date
623 self.start_date, self.due_date = date, date + duration
631 self.start_date, self.due_date = date, date + duration
624 save
632 save
625 end
633 end
626 else
634 else
627 leaves.each do |leaf|
635 leaves.each do |leaf|
628 leaf.reschedule_after(date)
636 leaf.reschedule_after(date)
629 end
637 end
630 end
638 end
631 end
639 end
632
640
633 def <=>(issue)
641 def <=>(issue)
634 if issue.nil?
642 if issue.nil?
635 -1
643 -1
636 elsif root_id != issue.root_id
644 elsif root_id != issue.root_id
637 (root_id || 0) <=> (issue.root_id || 0)
645 (root_id || 0) <=> (issue.root_id || 0)
638 else
646 else
639 (lft || 0) <=> (issue.lft || 0)
647 (lft || 0) <=> (issue.lft || 0)
640 end
648 end
641 end
649 end
642
650
643 def to_s
651 def to_s
644 "#{tracker} ##{id}: #{subject}"
652 "#{tracker} ##{id}: #{subject}"
645 end
653 end
646
654
647 # Returns a string of css classes that apply to the issue
655 # Returns a string of css classes that apply to the issue
648 def css_classes
656 def css_classes
649 s = "issue status-#{status.position} priority-#{priority.position}"
657 s = "issue status-#{status.position} priority-#{priority.position}"
650 s << ' closed' if closed?
658 s << ' closed' if closed?
651 s << ' overdue' if overdue?
659 s << ' overdue' if overdue?
652 s << ' child' if child?
660 s << ' child' if child?
653 s << ' parent' unless leaf?
661 s << ' parent' unless leaf?
654 s << ' private' if is_private?
662 s << ' private' if is_private?
655 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
663 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
656 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
664 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
657 s
665 s
658 end
666 end
659
667
660 # Saves an issue, time_entry, attachments, and a journal from the parameters
668 # Saves an issue, time_entry, attachments, and a journal from the parameters
661 # Returns false if save fails
669 # Returns false if save fails
662 def save_issue_with_child_records(params, existing_time_entry=nil)
670 def save_issue_with_child_records(params, existing_time_entry=nil)
663 Issue.transaction do
671 Issue.transaction do
664 if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, project)
672 if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, project)
665 @time_entry = existing_time_entry || TimeEntry.new
673 @time_entry = existing_time_entry || TimeEntry.new
666 @time_entry.project = project
674 @time_entry.project = project
667 @time_entry.issue = self
675 @time_entry.issue = self
668 @time_entry.user = User.current
676 @time_entry.user = User.current
669 @time_entry.spent_on = User.current.today
677 @time_entry.spent_on = User.current.today
670 @time_entry.attributes = params[:time_entry]
678 @time_entry.attributes = params[:time_entry]
671 self.time_entries << @time_entry
679 self.time_entries << @time_entry
672 end
680 end
673
681
674 if valid?
682 if valid?
675 attachments = Attachment.attach_files(self, params[:attachments])
683 attachments = Attachment.attach_files(self, params[:attachments])
676 # TODO: Rename hook
684 # TODO: Rename hook
677 Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
685 Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
678 begin
686 begin
679 if save
687 if save
680 # TODO: Rename hook
688 # TODO: Rename hook
681 Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
689 Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
682 else
690 else
683 raise ActiveRecord::Rollback
691 raise ActiveRecord::Rollback
684 end
692 end
685 rescue ActiveRecord::StaleObjectError
693 rescue ActiveRecord::StaleObjectError
686 attachments[:files].each(&:destroy)
694 attachments[:files].each(&:destroy)
687 errors.add :base, l(:notice_locking_conflict)
695 errors.add :base, l(:notice_locking_conflict)
688 raise ActiveRecord::Rollback
696 raise ActiveRecord::Rollback
689 end
697 end
690 end
698 end
691 end
699 end
692 end
700 end
693
701
694 # Unassigns issues from +version+ if it's no longer shared with issue's project
702 # Unassigns issues from +version+ if it's no longer shared with issue's project
695 def self.update_versions_from_sharing_change(version)
703 def self.update_versions_from_sharing_change(version)
696 # Update issues assigned to the version
704 # Update issues assigned to the version
697 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
705 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
698 end
706 end
699
707
700 # Unassigns issues from versions that are no longer shared
708 # Unassigns issues from versions that are no longer shared
701 # after +project+ was moved
709 # after +project+ was moved
702 def self.update_versions_from_hierarchy_change(project)
710 def self.update_versions_from_hierarchy_change(project)
703 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
711 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
704 # Update issues of the moved projects and issues assigned to a version of a moved project
712 # Update issues of the moved projects and issues assigned to a version of a moved project
705 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
713 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
706 end
714 end
707
715
708 def parent_issue_id=(arg)
716 def parent_issue_id=(arg)
709 parent_issue_id = arg.blank? ? nil : arg.to_i
717 parent_issue_id = arg.blank? ? nil : arg.to_i
710 if parent_issue_id && @parent_issue = Issue.find_by_id(parent_issue_id)
718 if parent_issue_id && @parent_issue = Issue.find_by_id(parent_issue_id)
711 @parent_issue.id
719 @parent_issue.id
712 else
720 else
713 @parent_issue = nil
721 @parent_issue = nil
714 nil
722 nil
715 end
723 end
716 end
724 end
717
725
718 def parent_issue_id
726 def parent_issue_id
719 if instance_variable_defined? :@parent_issue
727 if instance_variable_defined? :@parent_issue
720 @parent_issue.nil? ? nil : @parent_issue.id
728 @parent_issue.nil? ? nil : @parent_issue.id
721 else
729 else
722 parent_id
730 parent_id
723 end
731 end
724 end
732 end
725
733
726 # Extracted from the ReportsController.
734 # Extracted from the ReportsController.
727 def self.by_tracker(project)
735 def self.by_tracker(project)
728 count_and_group_by(:project => project,
736 count_and_group_by(:project => project,
729 :field => 'tracker_id',
737 :field => 'tracker_id',
730 :joins => Tracker.table_name)
738 :joins => Tracker.table_name)
731 end
739 end
732
740
733 def self.by_version(project)
741 def self.by_version(project)
734 count_and_group_by(:project => project,
742 count_and_group_by(:project => project,
735 :field => 'fixed_version_id',
743 :field => 'fixed_version_id',
736 :joins => Version.table_name)
744 :joins => Version.table_name)
737 end
745 end
738
746
739 def self.by_priority(project)
747 def self.by_priority(project)
740 count_and_group_by(:project => project,
748 count_and_group_by(:project => project,
741 :field => 'priority_id',
749 :field => 'priority_id',
742 :joins => IssuePriority.table_name)
750 :joins => IssuePriority.table_name)
743 end
751 end
744
752
745 def self.by_category(project)
753 def self.by_category(project)
746 count_and_group_by(:project => project,
754 count_and_group_by(:project => project,
747 :field => 'category_id',
755 :field => 'category_id',
748 :joins => IssueCategory.table_name)
756 :joins => IssueCategory.table_name)
749 end
757 end
750
758
751 def self.by_assigned_to(project)
759 def self.by_assigned_to(project)
752 count_and_group_by(:project => project,
760 count_and_group_by(:project => project,
753 :field => 'assigned_to_id',
761 :field => 'assigned_to_id',
754 :joins => User.table_name)
762 :joins => User.table_name)
755 end
763 end
756
764
757 def self.by_author(project)
765 def self.by_author(project)
758 count_and_group_by(:project => project,
766 count_and_group_by(:project => project,
759 :field => 'author_id',
767 :field => 'author_id',
760 :joins => User.table_name)
768 :joins => User.table_name)
761 end
769 end
762
770
763 def self.by_subproject(project)
771 def self.by_subproject(project)
764 ActiveRecord::Base.connection.select_all("select s.id as status_id,
772 ActiveRecord::Base.connection.select_all("select s.id as status_id,
765 s.is_closed as closed,
773 s.is_closed as closed,
766 #{Issue.table_name}.project_id as project_id,
774 #{Issue.table_name}.project_id as project_id,
767 count(#{Issue.table_name}.id) as total
775 count(#{Issue.table_name}.id) as total
768 from
776 from
769 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s
777 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s
770 where
778 where
771 #{Issue.table_name}.status_id=s.id
779 #{Issue.table_name}.status_id=s.id
772 and #{Issue.table_name}.project_id = #{Project.table_name}.id
780 and #{Issue.table_name}.project_id = #{Project.table_name}.id
773 and #{visible_condition(User.current, :project => project, :with_subprojects => true)}
781 and #{visible_condition(User.current, :project => project, :with_subprojects => true)}
774 and #{Issue.table_name}.project_id <> #{project.id}
782 and #{Issue.table_name}.project_id <> #{project.id}
775 group by s.id, s.is_closed, #{Issue.table_name}.project_id") if project.descendants.active.any?
783 group by s.id, s.is_closed, #{Issue.table_name}.project_id") if project.descendants.active.any?
776 end
784 end
777 # End ReportsController extraction
785 # End ReportsController extraction
778
786
779 # Returns an array of projects that user can assign the issue to
787 # Returns an array of projects that user can assign the issue to
780 def allowed_target_projects(user=User.current)
788 def allowed_target_projects(user=User.current)
781 if new_record?
789 if new_record?
782 Project.all(:conditions => Project.allowed_to_condition(user, :add_issues))
790 Project.all(:conditions => Project.allowed_to_condition(user, :add_issues))
783 else
791 else
784 self.class.allowed_target_projects_on_move(user)
792 self.class.allowed_target_projects_on_move(user)
785 end
793 end
786 end
794 end
787
795
788 # Returns an array of projects that user can move issues to
796 # Returns an array of projects that user can move issues to
789 def self.allowed_target_projects_on_move(user=User.current)
797 def self.allowed_target_projects_on_move(user=User.current)
790 projects = []
798 projects = []
791 if user.admin?
799 if user.admin?
792 # admin is allowed to move issues to any active (visible) project
800 # admin is allowed to move issues to any active (visible) project
793 projects = Project.visible(user).all
801 projects = Project.visible(user).all
794 elsif user.logged?
802 elsif user.logged?
795 if Role.non_member.allowed_to?(:move_issues)
803 if Role.non_member.allowed_to?(:move_issues)
796 projects = Project.visible(user).all
804 projects = Project.visible(user).all
797 else
805 else
798 user.memberships.each {|m| projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}}
806 user.memberships.each {|m| projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}}
799 end
807 end
800 end
808 end
801 projects
809 projects
802 end
810 end
803
811
804 private
812 private
805
813
806 def after_project_change
814 def after_project_change
807 # Update project_id on related time entries
815 # Update project_id on related time entries
808 TimeEntry.update_all(["project_id = ?", project_id], {:issue_id => id})
816 TimeEntry.update_all(["project_id = ?", project_id], {:issue_id => id})
809
817
810 # Delete issue relations
818 # Delete issue relations
811 unless Setting.cross_project_issue_relations?
819 unless Setting.cross_project_issue_relations?
812 relations_from.clear
820 relations_from.clear
813 relations_to.clear
821 relations_to.clear
814 end
822 end
815
823
816 # Move subtasks
824 # Move subtasks
817 children.each do |child|
825 children.each do |child|
818 # Change project and keep project
826 # Change project and keep project
819 child.send :project=, project, true
827 child.send :project=, project, true
820 unless child.save
828 unless child.save
821 raise ActiveRecord::Rollback
829 raise ActiveRecord::Rollback
822 end
830 end
823 end
831 end
824 end
832 end
825
833
826 def update_nested_set_attributes
834 def update_nested_set_attributes
827 if root_id.nil?
835 if root_id.nil?
828 # issue was just created
836 # issue was just created
829 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
837 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
830 set_default_left_and_right
838 set_default_left_and_right
831 Issue.update_all("root_id = #{root_id}, lft = #{lft}, rgt = #{rgt}", ["id = ?", id])
839 Issue.update_all("root_id = #{root_id}, lft = #{lft}, rgt = #{rgt}", ["id = ?", id])
832 if @parent_issue
840 if @parent_issue
833 move_to_child_of(@parent_issue)
841 move_to_child_of(@parent_issue)
834 end
842 end
835 reload
843 reload
836 elsif parent_issue_id != parent_id
844 elsif parent_issue_id != parent_id
837 former_parent_id = parent_id
845 former_parent_id = parent_id
838 # moving an existing issue
846 # moving an existing issue
839 if @parent_issue && @parent_issue.root_id == root_id
847 if @parent_issue && @parent_issue.root_id == root_id
840 # inside the same tree
848 # inside the same tree
841 move_to_child_of(@parent_issue)
849 move_to_child_of(@parent_issue)
842 else
850 else
843 # to another tree
851 # to another tree
844 unless root?
852 unless root?
845 move_to_right_of(root)
853 move_to_right_of(root)
846 reload
854 reload
847 end
855 end
848 old_root_id = root_id
856 old_root_id = root_id
849 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
857 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
850 target_maxright = nested_set_scope.maximum(right_column_name) || 0
858 target_maxright = nested_set_scope.maximum(right_column_name) || 0
851 offset = target_maxright + 1 - lft
859 offset = target_maxright + 1 - lft
852 Issue.update_all("root_id = #{root_id}, lft = lft + #{offset}, rgt = rgt + #{offset}",
860 Issue.update_all("root_id = #{root_id}, lft = lft + #{offset}, rgt = rgt + #{offset}",
853 ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
861 ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
854 self[left_column_name] = lft + offset
862 self[left_column_name] = lft + offset
855 self[right_column_name] = rgt + offset
863 self[right_column_name] = rgt + offset
856 if @parent_issue
864 if @parent_issue
857 move_to_child_of(@parent_issue)
865 move_to_child_of(@parent_issue)
858 end
866 end
859 end
867 end
860 reload
868 reload
861 # delete invalid relations of all descendants
869 # delete invalid relations of all descendants
862 self_and_descendants.each do |issue|
870 self_and_descendants.each do |issue|
863 issue.relations.each do |relation|
871 issue.relations.each do |relation|
864 relation.destroy unless relation.valid?
872 relation.destroy unless relation.valid?
865 end
873 end
866 end
874 end
867 # update former parent
875 # update former parent
868 recalculate_attributes_for(former_parent_id) if former_parent_id
876 recalculate_attributes_for(former_parent_id) if former_parent_id
869 end
877 end
870 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
878 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
871 end
879 end
872
880
873 def update_parent_attributes
881 def update_parent_attributes
874 recalculate_attributes_for(parent_id) if parent_id
882 recalculate_attributes_for(parent_id) if parent_id
875 end
883 end
876
884
877 def recalculate_attributes_for(issue_id)
885 def recalculate_attributes_for(issue_id)
878 if issue_id && p = Issue.find_by_id(issue_id)
886 if issue_id && p = Issue.find_by_id(issue_id)
879 # priority = highest priority of children
887 # priority = highest priority of children
880 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :joins => :priority)
888 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :joins => :priority)
881 p.priority = IssuePriority.find_by_position(priority_position)
889 p.priority = IssuePriority.find_by_position(priority_position)
882 end
890 end
883
891
884 # start/due dates = lowest/highest dates of children
892 # start/due dates = lowest/highest dates of children
885 p.start_date = p.children.minimum(:start_date)
893 p.start_date = p.children.minimum(:start_date)
886 p.due_date = p.children.maximum(:due_date)
894 p.due_date = p.children.maximum(:due_date)
887 if p.start_date && p.due_date && p.due_date < p.start_date
895 if p.start_date && p.due_date && p.due_date < p.start_date
888 p.start_date, p.due_date = p.due_date, p.start_date
896 p.start_date, p.due_date = p.due_date, p.start_date
889 end
897 end
890
898
891 # done ratio = weighted average ratio of leaves
899 # done ratio = weighted average ratio of leaves
892 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
900 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
893 leaves_count = p.leaves.count
901 leaves_count = p.leaves.count
894 if leaves_count > 0
902 if leaves_count > 0
895 average = p.leaves.average(:estimated_hours).to_f
903 average = p.leaves.average(:estimated_hours).to_f
896 if average == 0
904 if average == 0
897 average = 1
905 average = 1
898 end
906 end
899 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
907 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
900 progress = done / (average * leaves_count)
908 progress = done / (average * leaves_count)
901 p.done_ratio = progress.round
909 p.done_ratio = progress.round
902 end
910 end
903 end
911 end
904
912
905 # estimate = sum of leaves estimates
913 # estimate = sum of leaves estimates
906 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
914 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
907 p.estimated_hours = nil if p.estimated_hours == 0.0
915 p.estimated_hours = nil if p.estimated_hours == 0.0
908
916
909 # ancestors will be recursively updated
917 # ancestors will be recursively updated
910 p.save(false)
918 p.save(false)
911 end
919 end
912 end
920 end
913
921
914 # Update issues so their versions are not pointing to a
922 # Update issues so their versions are not pointing to a
915 # fixed_version that is not shared with the issue's project
923 # fixed_version that is not shared with the issue's project
916 def self.update_versions(conditions=nil)
924 def self.update_versions(conditions=nil)
917 # Only need to update issues with a fixed_version from
925 # Only need to update issues with a fixed_version from
918 # a different project and that is not systemwide shared
926 # a different project and that is not systemwide shared
919 Issue.scoped(:conditions => conditions).all(
927 Issue.scoped(:conditions => conditions).all(
920 :conditions => "#{Issue.table_name}.fixed_version_id IS NOT NULL" +
928 :conditions => "#{Issue.table_name}.fixed_version_id IS NOT NULL" +
921 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
929 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
922 " AND #{Version.table_name}.sharing <> 'system'",
930 " AND #{Version.table_name}.sharing <> 'system'",
923 :include => [:project, :fixed_version]
931 :include => [:project, :fixed_version]
924 ).each do |issue|
932 ).each do |issue|
925 next if issue.project.nil? || issue.fixed_version.nil?
933 next if issue.project.nil? || issue.fixed_version.nil?
926 unless issue.project.shared_versions.include?(issue.fixed_version)
934 unless issue.project.shared_versions.include?(issue.fixed_version)
927 issue.init_journal(User.current)
935 issue.init_journal(User.current)
928 issue.fixed_version = nil
936 issue.fixed_version = nil
929 issue.save
937 issue.save
930 end
938 end
931 end
939 end
932 end
940 end
933
941
934 # Callback on attachment deletion
942 # Callback on attachment deletion
935 def attachment_added(obj)
943 def attachment_added(obj)
936 if @current_journal && !obj.new_record?
944 if @current_journal && !obj.new_record?
937 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :value => obj.filename)
945 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :value => obj.filename)
938 end
946 end
939 end
947 end
940
948
941 # Callback on attachment deletion
949 # Callback on attachment deletion
942 def attachment_removed(obj)
950 def attachment_removed(obj)
943 journal = init_journal(User.current)
951 journal = init_journal(User.current)
944 journal.details << JournalDetail.new(:property => 'attachment',
952 journal.details << JournalDetail.new(:property => 'attachment',
945 :prop_key => obj.id,
953 :prop_key => obj.id,
946 :old_value => obj.filename)
954 :old_value => obj.filename)
947 journal.save
955 journal.save
948 end
956 end
949
957
950 # Default assignment based on category
958 # Default assignment based on category
951 def default_assign
959 def default_assign
952 if assigned_to.nil? && category && category.assigned_to
960 if assigned_to.nil? && category && category.assigned_to
953 self.assigned_to = category.assigned_to
961 self.assigned_to = category.assigned_to
954 end
962 end
955 end
963 end
956
964
957 # Updates start/due dates of following issues
965 # Updates start/due dates of following issues
958 def reschedule_following_issues
966 def reschedule_following_issues
959 if start_date_changed? || due_date_changed?
967 if start_date_changed? || due_date_changed?
960 relations_from.each do |relation|
968 relations_from.each do |relation|
961 relation.set_issue_to_dates
969 relation.set_issue_to_dates
962 end
970 end
963 end
971 end
964 end
972 end
965
973
966 # Closes duplicates if the issue is being closed
974 # Closes duplicates if the issue is being closed
967 def close_duplicates
975 def close_duplicates
968 if closing?
976 if closing?
969 duplicates.each do |duplicate|
977 duplicates.each do |duplicate|
970 # Reload is need in case the duplicate was updated by a previous duplicate
978 # Reload is need in case the duplicate was updated by a previous duplicate
971 duplicate.reload
979 duplicate.reload
972 # Don't re-close it if it's already closed
980 # Don't re-close it if it's already closed
973 next if duplicate.closed?
981 next if duplicate.closed?
974 # Same user and notes
982 # Same user and notes
975 if @current_journal
983 if @current_journal
976 duplicate.init_journal(@current_journal.user, @current_journal.notes)
984 duplicate.init_journal(@current_journal.user, @current_journal.notes)
977 end
985 end
978 duplicate.update_attribute :status, self.status
986 duplicate.update_attribute :status, self.status
979 end
987 end
980 end
988 end
981 end
989 end
982
990
983 # Saves the changes in a Journal
991 # Saves the changes in a Journal
984 # Called after_save
992 # Called after_save
985 def create_journal
993 def create_journal
986 if @current_journal
994 if @current_journal
987 # attributes changes
995 # attributes changes
988 if @attributes_before_change
996 if @attributes_before_change
989 (Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on)).each {|c|
997 (Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on)).each {|c|
990 before = @attributes_before_change[c]
998 before = @attributes_before_change[c]
991 after = send(c)
999 after = send(c)
992 next if before == after || (before.blank? && after.blank?)
1000 next if before == after || (before.blank? && after.blank?)
993 @current_journal.details << JournalDetail.new(:property => 'attr',
1001 @current_journal.details << JournalDetail.new(:property => 'attr',
994 :prop_key => c,
1002 :prop_key => c,
995 :old_value => before,
1003 :old_value => before,
996 :value => after)
1004 :value => after)
997 }
1005 }
998 end
1006 end
999 if @custom_values_before_change
1007 if @custom_values_before_change
1000 # custom fields changes
1008 # custom fields changes
1001 custom_values.each {|c|
1009 custom_values.each {|c|
1002 before = @custom_values_before_change[c.custom_field_id]
1010 before = @custom_values_before_change[c.custom_field_id]
1003 after = c.value
1011 after = c.value
1004 next if before == after || (before.blank? && after.blank?)
1012 next if before == after || (before.blank? && after.blank?)
1005 @current_journal.details << JournalDetail.new(:property => 'cf',
1013 @current_journal.details << JournalDetail.new(:property => 'cf',
1006 :prop_key => c.custom_field_id,
1014 :prop_key => c.custom_field_id,
1007 :old_value => before,
1015 :old_value => before,
1008 :value => after)
1016 :value => after)
1009 }
1017 }
1010 end
1018 end
1011 @current_journal.save
1019 @current_journal.save
1012 # reset current journal
1020 # reset current journal
1013 init_journal @current_journal.user, @current_journal.notes
1021 init_journal @current_journal.user, @current_journal.notes
1014 end
1022 end
1015 end
1023 end
1016
1024
1017 # Query generator for selecting groups of issue counts for a project
1025 # Query generator for selecting groups of issue counts for a project
1018 # based on specific criteria
1026 # based on specific criteria
1019 #
1027 #
1020 # Options
1028 # Options
1021 # * project - Project to search in.
1029 # * project - Project to search in.
1022 # * field - String. Issue field to key off of in the grouping.
1030 # * field - String. Issue field to key off of in the grouping.
1023 # * joins - String. The table name to join against.
1031 # * joins - String. The table name to join against.
1024 def self.count_and_group_by(options)
1032 def self.count_and_group_by(options)
1025 project = options.delete(:project)
1033 project = options.delete(:project)
1026 select_field = options.delete(:field)
1034 select_field = options.delete(:field)
1027 joins = options.delete(:joins)
1035 joins = options.delete(:joins)
1028
1036
1029 where = "#{Issue.table_name}.#{select_field}=j.id"
1037 where = "#{Issue.table_name}.#{select_field}=j.id"
1030
1038
1031 ActiveRecord::Base.connection.select_all("select s.id as status_id,
1039 ActiveRecord::Base.connection.select_all("select s.id as status_id,
1032 s.is_closed as closed,
1040 s.is_closed as closed,
1033 j.id as #{select_field},
1041 j.id as #{select_field},
1034 count(#{Issue.table_name}.id) as total
1042 count(#{Issue.table_name}.id) as total
1035 from
1043 from
1036 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s, #{joins} j
1044 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s, #{joins} j
1037 where
1045 where
1038 #{Issue.table_name}.status_id=s.id
1046 #{Issue.table_name}.status_id=s.id
1039 and #{where}
1047 and #{where}
1040 and #{Issue.table_name}.project_id=#{Project.table_name}.id
1048 and #{Issue.table_name}.project_id=#{Project.table_name}.id
1041 and #{visible_condition(User.current, :project => project)}
1049 and #{visible_condition(User.current, :project => project)}
1042 group by s.id, s.is_closed, j.id")
1050 group by s.id, s.is_closed, j.id")
1043 end
1051 end
1044 end
1052 end
@@ -1,647 +1,647
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require "digest/sha1"
18 require "digest/sha1"
19
19
20 class User < Principal
20 class User < Principal
21 include Redmine::SafeAttributes
21 include Redmine::SafeAttributes
22
22
23 # Account statuses
23 # Account statuses
24 STATUS_ANONYMOUS = 0
24 STATUS_ANONYMOUS = 0
25 STATUS_ACTIVE = 1
25 STATUS_ACTIVE = 1
26 STATUS_REGISTERED = 2
26 STATUS_REGISTERED = 2
27 STATUS_LOCKED = 3
27 STATUS_LOCKED = 3
28
28
29 # Different ways of displaying/sorting users
29 # Different ways of displaying/sorting users
30 USER_FORMATS = {
30 USER_FORMATS = {
31 :firstname_lastname => {:string => '#{firstname} #{lastname}', :order => %w(firstname lastname id)},
31 :firstname_lastname => {:string => '#{firstname} #{lastname}', :order => %w(firstname lastname id)},
32 :firstname => {:string => '#{firstname}', :order => %w(firstname id)},
32 :firstname => {:string => '#{firstname}', :order => %w(firstname id)},
33 :lastname_firstname => {:string => '#{lastname} #{firstname}', :order => %w(lastname firstname id)},
33 :lastname_firstname => {:string => '#{lastname} #{firstname}', :order => %w(lastname firstname id)},
34 :lastname_coma_firstname => {:string => '#{lastname}, #{firstname}', :order => %w(lastname firstname id)},
34 :lastname_coma_firstname => {:string => '#{lastname}, #{firstname}', :order => %w(lastname firstname id)},
35 :username => {:string => '#{login}', :order => %w(login id)},
35 :username => {:string => '#{login}', :order => %w(login id)},
36 }
36 }
37
37
38 MAIL_NOTIFICATION_OPTIONS = [
38 MAIL_NOTIFICATION_OPTIONS = [
39 ['all', :label_user_mail_option_all],
39 ['all', :label_user_mail_option_all],
40 ['selected', :label_user_mail_option_selected],
40 ['selected', :label_user_mail_option_selected],
41 ['only_my_events', :label_user_mail_option_only_my_events],
41 ['only_my_events', :label_user_mail_option_only_my_events],
42 ['only_assigned', :label_user_mail_option_only_assigned],
42 ['only_assigned', :label_user_mail_option_only_assigned],
43 ['only_owner', :label_user_mail_option_only_owner],
43 ['only_owner', :label_user_mail_option_only_owner],
44 ['none', :label_user_mail_option_none]
44 ['none', :label_user_mail_option_none]
45 ]
45 ]
46
46
47 has_and_belongs_to_many :groups, :after_add => Proc.new {|user, group| group.user_added(user)},
47 has_and_belongs_to_many :groups, :after_add => Proc.new {|user, group| group.user_added(user)},
48 :after_remove => Proc.new {|user, group| group.user_removed(user)}
48 :after_remove => Proc.new {|user, group| group.user_removed(user)}
49 has_many :changesets, :dependent => :nullify
49 has_many :changesets, :dependent => :nullify
50 has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
50 has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
51 has_one :rss_token, :class_name => 'Token', :conditions => "action='feeds'"
51 has_one :rss_token, :class_name => 'Token', :conditions => "action='feeds'"
52 has_one :api_token, :class_name => 'Token', :conditions => "action='api'"
52 has_one :api_token, :class_name => 'Token', :conditions => "action='api'"
53 belongs_to :auth_source
53 belongs_to :auth_source
54
54
55 # Active non-anonymous users scope
55 # Active non-anonymous users scope
56 named_scope :active, :conditions => "#{User.table_name}.status = #{STATUS_ACTIVE}"
56 named_scope :active, :conditions => "#{User.table_name}.status = #{STATUS_ACTIVE}"
57 named_scope :logged, :conditions => "#{User.table_name}.status <> #{STATUS_ANONYMOUS}"
57 named_scope :logged, :conditions => "#{User.table_name}.status <> #{STATUS_ANONYMOUS}"
58 named_scope :status, lambda {|arg| arg.blank? ? {} : {:conditions => {:status => arg.to_i}} }
58 named_scope :status, lambda {|arg| arg.blank? ? {} : {:conditions => {:status => arg.to_i}} }
59 named_scope :like, lambda {|arg|
59 named_scope :like, lambda {|arg|
60 if arg.blank?
60 if arg.blank?
61 {}
61 {}
62 else
62 else
63 pattern = "%#{arg.to_s.strip.downcase}%"
63 pattern = "%#{arg.to_s.strip.downcase}%"
64 {:conditions => ["LOWER(login) LIKE :p OR LOWER(firstname) LIKE :p OR LOWER(lastname) LIKE :p OR LOWER(mail) LIKE :p", {:p => pattern}]}
64 {:conditions => ["LOWER(login) LIKE :p OR LOWER(firstname) LIKE :p OR LOWER(lastname) LIKE :p OR LOWER(mail) LIKE :p", {:p => pattern}]}
65 end
65 end
66 }
66 }
67
67
68 acts_as_customizable
68 acts_as_customizable
69
69
70 attr_accessor :password, :password_confirmation
70 attr_accessor :password, :password_confirmation
71 attr_accessor :last_before_login_on
71 attr_accessor :last_before_login_on
72 # Prevents unauthorized assignments
72 # Prevents unauthorized assignments
73 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password
73 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password
74
74
75 validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
75 validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
76 validates_uniqueness_of :login, :if => Proc.new { |user| !user.login.blank? }, :case_sensitive => false
76 validates_uniqueness_of :login, :if => Proc.new { |user| !user.login.blank? }, :case_sensitive => false
77 validates_uniqueness_of :mail, :if => Proc.new { |user| !user.mail.blank? }, :case_sensitive => false
77 validates_uniqueness_of :mail, :if => Proc.new { |user| !user.mail.blank? }, :case_sensitive => false
78 # Login must contain lettres, numbers, underscores only
78 # Login must contain lettres, numbers, underscores only
79 validates_format_of :login, :with => /^[a-z0-9_\-@\.]*$/i
79 validates_format_of :login, :with => /^[a-z0-9_\-@\.]*$/i
80 validates_length_of :login, :maximum => 30
80 validates_length_of :login, :maximum => 30
81 validates_length_of :firstname, :lastname, :maximum => 30
81 validates_length_of :firstname, :lastname, :maximum => 30
82 validates_format_of :mail, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i, :allow_blank => true
82 validates_format_of :mail, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i, :allow_blank => true
83 validates_length_of :mail, :maximum => 60, :allow_nil => true
83 validates_length_of :mail, :maximum => 60, :allow_nil => true
84 validates_confirmation_of :password, :allow_nil => true
84 validates_confirmation_of :password, :allow_nil => true
85 validates_inclusion_of :mail_notification, :in => MAIL_NOTIFICATION_OPTIONS.collect(&:first), :allow_blank => true
85 validates_inclusion_of :mail_notification, :in => MAIL_NOTIFICATION_OPTIONS.collect(&:first), :allow_blank => true
86 validate :validate_password_length
86 validate :validate_password_length
87
87
88 before_create :set_mail_notification
88 before_create :set_mail_notification
89 before_save :update_hashed_password
89 before_save :update_hashed_password
90 before_destroy :remove_references_before_destroy
90 before_destroy :remove_references_before_destroy
91
91
92 named_scope :in_group, lambda {|group|
92 named_scope :in_group, lambda {|group|
93 group_id = group.is_a?(Group) ? group.id : group.to_i
93 group_id = group.is_a?(Group) ? group.id : group.to_i
94 { :conditions => ["#{User.table_name}.id IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id] }
94 { :conditions => ["#{User.table_name}.id IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id] }
95 }
95 }
96 named_scope :not_in_group, lambda {|group|
96 named_scope :not_in_group, lambda {|group|
97 group_id = group.is_a?(Group) ? group.id : group.to_i
97 group_id = group.is_a?(Group) ? group.id : group.to_i
98 { :conditions => ["#{User.table_name}.id NOT IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id] }
98 { :conditions => ["#{User.table_name}.id NOT IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id] }
99 }
99 }
100
100
101 def set_mail_notification
101 def set_mail_notification
102 self.mail_notification = Setting.default_notification_option if self.mail_notification.blank?
102 self.mail_notification = Setting.default_notification_option if self.mail_notification.blank?
103 true
103 true
104 end
104 end
105
105
106 def update_hashed_password
106 def update_hashed_password
107 # update hashed_password if password was set
107 # update hashed_password if password was set
108 if self.password && self.auth_source_id.blank?
108 if self.password && self.auth_source_id.blank?
109 salt_password(password)
109 salt_password(password)
110 end
110 end
111 end
111 end
112
112
113 def reload(*args)
113 def reload(*args)
114 @name = nil
114 @name = nil
115 @projects_by_role = nil
115 @projects_by_role = nil
116 super
116 super
117 end
117 end
118
118
119 def mail=(arg)
119 def mail=(arg)
120 write_attribute(:mail, arg.to_s.strip)
120 write_attribute(:mail, arg.to_s.strip)
121 end
121 end
122
122
123 def identity_url=(url)
123 def identity_url=(url)
124 if url.blank?
124 if url.blank?
125 write_attribute(:identity_url, '')
125 write_attribute(:identity_url, '')
126 else
126 else
127 begin
127 begin
128 write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url))
128 write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url))
129 rescue OpenIdAuthentication::InvalidOpenId
129 rescue OpenIdAuthentication::InvalidOpenId
130 # Invlaid url, don't save
130 # Invlaid url, don't save
131 end
131 end
132 end
132 end
133 self.read_attribute(:identity_url)
133 self.read_attribute(:identity_url)
134 end
134 end
135
135
136 # Returns the user that matches provided login and password, or nil
136 # Returns the user that matches provided login and password, or nil
137 def self.try_to_login(login, password)
137 def self.try_to_login(login, password)
138 # Make sure no one can sign in with an empty password
138 # Make sure no one can sign in with an empty password
139 return nil if password.to_s.empty?
139 return nil if password.to_s.empty?
140 user = find_by_login(login)
140 user = find_by_login(login)
141 if user
141 if user
142 # user is already in local database
142 # user is already in local database
143 return nil if !user.active?
143 return nil if !user.active?
144 if user.auth_source
144 if user.auth_source
145 # user has an external authentication method
145 # user has an external authentication method
146 return nil unless user.auth_source.authenticate(login, password)
146 return nil unless user.auth_source.authenticate(login, password)
147 else
147 else
148 # authentication with local password
148 # authentication with local password
149 return nil unless user.check_password?(password)
149 return nil unless user.check_password?(password)
150 end
150 end
151 else
151 else
152 # user is not yet registered, try to authenticate with available sources
152 # user is not yet registered, try to authenticate with available sources
153 attrs = AuthSource.authenticate(login, password)
153 attrs = AuthSource.authenticate(login, password)
154 if attrs
154 if attrs
155 user = new(attrs)
155 user = new(attrs)
156 user.login = login
156 user.login = login
157 user.language = Setting.default_language
157 user.language = Setting.default_language
158 if user.save
158 if user.save
159 user.reload
159 user.reload
160 logger.info("User '#{user.login}' created from external auth source: #{user.auth_source.type} - #{user.auth_source.name}") if logger && user.auth_source
160 logger.info("User '#{user.login}' created from external auth source: #{user.auth_source.type} - #{user.auth_source.name}") if logger && user.auth_source
161 end
161 end
162 end
162 end
163 end
163 end
164 user.update_attribute(:last_login_on, Time.now) if user && !user.new_record?
164 user.update_attribute(:last_login_on, Time.now) if user && !user.new_record?
165 user
165 user
166 rescue => text
166 rescue => text
167 raise text
167 raise text
168 end
168 end
169
169
170 # Returns the user who matches the given autologin +key+ or nil
170 # Returns the user who matches the given autologin +key+ or nil
171 def self.try_to_autologin(key)
171 def self.try_to_autologin(key)
172 tokens = Token.find_all_by_action_and_value('autologin', key)
172 tokens = Token.find_all_by_action_and_value('autologin', key)
173 # Make sure there's only 1 token that matches the key
173 # Make sure there's only 1 token that matches the key
174 if tokens.size == 1
174 if tokens.size == 1
175 token = tokens.first
175 token = tokens.first
176 if (token.created_on > Setting.autologin.to_i.day.ago) && token.user && token.user.active?
176 if (token.created_on > Setting.autologin.to_i.day.ago) && token.user && token.user.active?
177 token.user.update_attribute(:last_login_on, Time.now)
177 token.user.update_attribute(:last_login_on, Time.now)
178 token.user
178 token.user
179 end
179 end
180 end
180 end
181 end
181 end
182
182
183 def self.name_formatter(formatter = nil)
183 def self.name_formatter(formatter = nil)
184 USER_FORMATS[formatter || Setting.user_format] || USER_FORMATS[:firstname_lastname]
184 USER_FORMATS[formatter || Setting.user_format] || USER_FORMATS[:firstname_lastname]
185 end
185 end
186
186
187 # Returns an array of fields names than can be used to make an order statement for users
187 # Returns an array of fields names than can be used to make an order statement for users
188 # according to how user names are displayed
188 # according to how user names are displayed
189 # Examples:
189 # Examples:
190 #
190 #
191 # User.fields_for_order_statement => ['users.login', 'users.id']
191 # User.fields_for_order_statement => ['users.login', 'users.id']
192 # User.fields_for_order_statement('authors') => ['authors.login', 'authors.id']
192 # User.fields_for_order_statement('authors') => ['authors.login', 'authors.id']
193 def self.fields_for_order_statement(table=nil)
193 def self.fields_for_order_statement(table=nil)
194 table ||= table_name
194 table ||= table_name
195 name_formatter[:order].map {|field| "#{table}.#{field}"}
195 name_formatter[:order].map {|field| "#{table}.#{field}"}
196 end
196 end
197
197
198 # Return user's full name for display
198 # Return user's full name for display
199 def name(formatter = nil)
199 def name(formatter = nil)
200 f = self.class.name_formatter(formatter)
200 f = self.class.name_formatter(formatter)
201 if formatter
201 if formatter
202 eval('"' + f[:string] + '"')
202 eval('"' + f[:string] + '"')
203 else
203 else
204 @name ||= eval('"' + f[:string] + '"')
204 @name ||= eval('"' + f[:string] + '"')
205 end
205 end
206 end
206 end
207
207
208 def active?
208 def active?
209 self.status == STATUS_ACTIVE
209 self.status == STATUS_ACTIVE
210 end
210 end
211
211
212 def registered?
212 def registered?
213 self.status == STATUS_REGISTERED
213 self.status == STATUS_REGISTERED
214 end
214 end
215
215
216 def locked?
216 def locked?
217 self.status == STATUS_LOCKED
217 self.status == STATUS_LOCKED
218 end
218 end
219
219
220 def activate
220 def activate
221 self.status = STATUS_ACTIVE
221 self.status = STATUS_ACTIVE
222 end
222 end
223
223
224 def register
224 def register
225 self.status = STATUS_REGISTERED
225 self.status = STATUS_REGISTERED
226 end
226 end
227
227
228 def lock
228 def lock
229 self.status = STATUS_LOCKED
229 self.status = STATUS_LOCKED
230 end
230 end
231
231
232 def activate!
232 def activate!
233 update_attribute(:status, STATUS_ACTIVE)
233 update_attribute(:status, STATUS_ACTIVE)
234 end
234 end
235
235
236 def register!
236 def register!
237 update_attribute(:status, STATUS_REGISTERED)
237 update_attribute(:status, STATUS_REGISTERED)
238 end
238 end
239
239
240 def lock!
240 def lock!
241 update_attribute(:status, STATUS_LOCKED)
241 update_attribute(:status, STATUS_LOCKED)
242 end
242 end
243
243
244 # Returns true if +clear_password+ is the correct user's password, otherwise false
244 # Returns true if +clear_password+ is the correct user's password, otherwise false
245 def check_password?(clear_password)
245 def check_password?(clear_password)
246 if auth_source_id.present?
246 if auth_source_id.present?
247 auth_source.authenticate(self.login, clear_password)
247 auth_source.authenticate(self.login, clear_password)
248 else
248 else
249 User.hash_password("#{salt}#{User.hash_password clear_password}") == hashed_password
249 User.hash_password("#{salt}#{User.hash_password clear_password}") == hashed_password
250 end
250 end
251 end
251 end
252
252
253 # Generates a random salt and computes hashed_password for +clear_password+
253 # Generates a random salt and computes hashed_password for +clear_password+
254 # The hashed password is stored in the following form: SHA1(salt + SHA1(password))
254 # The hashed password is stored in the following form: SHA1(salt + SHA1(password))
255 def salt_password(clear_password)
255 def salt_password(clear_password)
256 self.salt = User.generate_salt
256 self.salt = User.generate_salt
257 self.hashed_password = User.hash_password("#{salt}#{User.hash_password clear_password}")
257 self.hashed_password = User.hash_password("#{salt}#{User.hash_password clear_password}")
258 end
258 end
259
259
260 # Does the backend storage allow this user to change their password?
260 # Does the backend storage allow this user to change their password?
261 def change_password_allowed?
261 def change_password_allowed?
262 return true if auth_source.nil?
262 return true if auth_source.nil?
263 return auth_source.allow_password_changes?
263 return auth_source.allow_password_changes?
264 end
264 end
265
265
266 # Generate and set a random password. Useful for automated user creation
266 # Generate and set a random password. Useful for automated user creation
267 # Based on Token#generate_token_value
267 # Based on Token#generate_token_value
268 #
268 #
269 def random_password
269 def random_password
270 chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
270 chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
271 password = ''
271 password = ''
272 40.times { |i| password << chars[rand(chars.size-1)] }
272 40.times { |i| password << chars[rand(chars.size-1)] }
273 self.password = password
273 self.password = password
274 self.password_confirmation = password
274 self.password_confirmation = password
275 self
275 self
276 end
276 end
277
277
278 def pref
278 def pref
279 self.preference ||= UserPreference.new(:user => self)
279 self.preference ||= UserPreference.new(:user => self)
280 end
280 end
281
281
282 def time_zone
282 def time_zone
283 @time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone])
283 @time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone])
284 end
284 end
285
285
286 def wants_comments_in_reverse_order?
286 def wants_comments_in_reverse_order?
287 self.pref[:comments_sorting] == 'desc'
287 self.pref[:comments_sorting] == 'desc'
288 end
288 end
289
289
290 # Return user's RSS key (a 40 chars long string), used to access feeds
290 # Return user's RSS key (a 40 chars long string), used to access feeds
291 def rss_key
291 def rss_key
292 token = self.rss_token || Token.create(:user => self, :action => 'feeds')
292 token = self.rss_token || Token.create(:user => self, :action => 'feeds')
293 token.value
293 token.value
294 end
294 end
295
295
296 # Return user's API key (a 40 chars long string), used to access the API
296 # Return user's API key (a 40 chars long string), used to access the API
297 def api_key
297 def api_key
298 token = self.api_token || self.create_api_token(:action => 'api')
298 token = self.api_token || self.create_api_token(:action => 'api')
299 token.value
299 token.value
300 end
300 end
301
301
302 # Return an array of project ids for which the user has explicitly turned mail notifications on
302 # Return an array of project ids for which the user has explicitly turned mail notifications on
303 def notified_projects_ids
303 def notified_projects_ids
304 @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
304 @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
305 end
305 end
306
306
307 def notified_project_ids=(ids)
307 def notified_project_ids=(ids)
308 Member.update_all("mail_notification = #{connection.quoted_false}", ['user_id = ?', id])
308 Member.update_all("mail_notification = #{connection.quoted_false}", ['user_id = ?', id])
309 Member.update_all("mail_notification = #{connection.quoted_true}", ['user_id = ? AND project_id IN (?)', id, ids]) if ids && !ids.empty?
309 Member.update_all("mail_notification = #{connection.quoted_true}", ['user_id = ? AND project_id IN (?)', id, ids]) if ids && !ids.empty?
310 @notified_projects_ids = nil
310 @notified_projects_ids = nil
311 notified_projects_ids
311 notified_projects_ids
312 end
312 end
313
313
314 def valid_notification_options
314 def valid_notification_options
315 self.class.valid_notification_options(self)
315 self.class.valid_notification_options(self)
316 end
316 end
317
317
318 # Only users that belong to more than 1 project can select projects for which they are notified
318 # Only users that belong to more than 1 project can select projects for which they are notified
319 def self.valid_notification_options(user=nil)
319 def self.valid_notification_options(user=nil)
320 # Note that @user.membership.size would fail since AR ignores
320 # Note that @user.membership.size would fail since AR ignores
321 # :include association option when doing a count
321 # :include association option when doing a count
322 if user.nil? || user.memberships.length < 1
322 if user.nil? || user.memberships.length < 1
323 MAIL_NOTIFICATION_OPTIONS.reject {|option| option.first == 'selected'}
323 MAIL_NOTIFICATION_OPTIONS.reject {|option| option.first == 'selected'}
324 else
324 else
325 MAIL_NOTIFICATION_OPTIONS
325 MAIL_NOTIFICATION_OPTIONS
326 end
326 end
327 end
327 end
328
328
329 # Find a user account by matching the exact login and then a case-insensitive
329 # Find a user account by matching the exact login and then a case-insensitive
330 # version. Exact matches will be given priority.
330 # version. Exact matches will be given priority.
331 def self.find_by_login(login)
331 def self.find_by_login(login)
332 # force string comparison to be case sensitive on MySQL
332 # force string comparison to be case sensitive on MySQL
333 type_cast = (ActiveRecord::Base.connection.adapter_name == 'MySQL') ? 'BINARY' : ''
333 type_cast = (ActiveRecord::Base.connection.adapter_name == 'MySQL') ? 'BINARY' : ''
334
334
335 # First look for an exact match
335 # First look for an exact match
336 user = first(:conditions => ["#{type_cast} login = ?", login])
336 user = first(:conditions => ["#{type_cast} login = ?", login])
337 # Fail over to case-insensitive if none was found
337 # Fail over to case-insensitive if none was found
338 user ||= first(:conditions => ["#{type_cast} LOWER(login) = ?", login.to_s.downcase])
338 user ||= first(:conditions => ["#{type_cast} LOWER(login) = ?", login.to_s.downcase])
339 end
339 end
340
340
341 def self.find_by_rss_key(key)
341 def self.find_by_rss_key(key)
342 token = Token.find_by_value(key)
342 token = Token.find_by_value(key)
343 token && token.user.active? ? token.user : nil
343 token && token.user.active? ? token.user : nil
344 end
344 end
345
345
346 def self.find_by_api_key(key)
346 def self.find_by_api_key(key)
347 token = Token.find_by_action_and_value('api', key)
347 token = Token.find_by_action_and_value('api', key)
348 token && token.user.active? ? token.user : nil
348 token && token.user.active? ? token.user : nil
349 end
349 end
350
350
351 # Makes find_by_mail case-insensitive
351 # Makes find_by_mail case-insensitive
352 def self.find_by_mail(mail)
352 def self.find_by_mail(mail)
353 find(:first, :conditions => ["LOWER(mail) = ?", mail.to_s.downcase])
353 find(:first, :conditions => ["LOWER(mail) = ?", mail.to_s.downcase])
354 end
354 end
355
355
356 def to_s
356 def to_s
357 name
357 name
358 end
358 end
359
359
360 # Returns the current day according to user's time zone
360 # Returns the current day according to user's time zone
361 def today
361 def today
362 if time_zone.nil?
362 if time_zone.nil?
363 Date.today
363 Date.today
364 else
364 else
365 Time.now.in_time_zone(time_zone).to_date
365 Time.now.in_time_zone(time_zone).to_date
366 end
366 end
367 end
367 end
368
368
369 def logged?
369 def logged?
370 true
370 true
371 end
371 end
372
372
373 def anonymous?
373 def anonymous?
374 !logged?
374 !logged?
375 end
375 end
376
376
377 # Return user's roles for project
377 # Return user's roles for project
378 def roles_for_project(project)
378 def roles_for_project(project)
379 roles = []
379 roles = []
380 # No role on archived projects
380 # No role on archived projects
381 return roles unless project && project.active?
381 return roles unless project && project.active?
382 if logged?
382 if logged?
383 # Find project membership
383 # Find project membership
384 membership = memberships.detect {|m| m.project_id == project.id}
384 membership = memberships.detect {|m| m.project_id == project.id}
385 if membership
385 if membership
386 roles = membership.roles
386 roles = membership.roles
387 else
387 else
388 @role_non_member ||= Role.non_member
388 @role_non_member ||= Role.non_member
389 roles << @role_non_member
389 roles << @role_non_member
390 end
390 end
391 else
391 else
392 @role_anonymous ||= Role.anonymous
392 @role_anonymous ||= Role.anonymous
393 roles << @role_anonymous
393 roles << @role_anonymous
394 end
394 end
395 roles
395 roles
396 end
396 end
397
397
398 # Return true if the user is a member of project
398 # Return true if the user is a member of project
399 def member_of?(project)
399 def member_of?(project)
400 !roles_for_project(project).detect {|role| role.member?}.nil?
400 !roles_for_project(project).detect {|role| role.member?}.nil?
401 end
401 end
402
402
403 # Returns a hash of user's projects grouped by roles
403 # Returns a hash of user's projects grouped by roles
404 def projects_by_role
404 def projects_by_role
405 return @projects_by_role if @projects_by_role
405 return @projects_by_role if @projects_by_role
406
406
407 @projects_by_role = Hash.new {|h,k| h[k]=[]}
407 @projects_by_role = Hash.new {|h,k| h[k]=[]}
408 memberships.each do |membership|
408 memberships.each do |membership|
409 membership.roles.each do |role|
409 membership.roles.each do |role|
410 @projects_by_role[role] << membership.project if membership.project
410 @projects_by_role[role] << membership.project if membership.project
411 end
411 end
412 end
412 end
413 @projects_by_role.each do |role, projects|
413 @projects_by_role.each do |role, projects|
414 projects.uniq!
414 projects.uniq!
415 end
415 end
416
416
417 @projects_by_role
417 @projects_by_role
418 end
418 end
419
419
420 # Returns true if user is arg or belongs to arg
420 # Returns true if user is arg or belongs to arg
421 def is_or_belongs_to?(arg)
421 def is_or_belongs_to?(arg)
422 if arg.is_a?(User)
422 if arg.is_a?(User)
423 self == arg
423 self == arg
424 elsif arg.is_a?(Group)
424 elsif arg.is_a?(Group)
425 arg.users.include?(self)
425 arg.users.include?(self)
426 else
426 else
427 false
427 false
428 end
428 end
429 end
429 end
430
430
431 # Return true if the user is allowed to do the specified action on a specific context
431 # Return true if the user is allowed to do the specified action on a specific context
432 # Action can be:
432 # Action can be:
433 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
433 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
434 # * a permission Symbol (eg. :edit_project)
434 # * a permission Symbol (eg. :edit_project)
435 # Context can be:
435 # Context can be:
436 # * a project : returns true if user is allowed to do the specified action on this project
436 # * a project : returns true if user is allowed to do the specified action on this project
437 # * an array of projects : returns true if user is allowed on every project
437 # * an array of projects : returns true if user is allowed on every project
438 # * nil with options[:global] set : check if user has at least one role allowed for this action,
438 # * nil with options[:global] set : check if user has at least one role allowed for this action,
439 # or falls back to Non Member / Anonymous permissions depending if the user is logged
439 # or falls back to Non Member / Anonymous permissions depending if the user is logged
440 def allowed_to?(action, context, options={}, &block)
440 def allowed_to?(action, context, options={}, &block)
441 if context && context.is_a?(Project)
441 if context && context.is_a?(Project)
442 # No action allowed on archived projects
442 # No action allowed on archived projects
443 return false unless context.active?
443 return false unless context.active?
444 # No action allowed on disabled modules
444 # No action allowed on disabled modules
445 return false unless context.allows_to?(action)
445 return false unless context.allows_to?(action)
446 # Admin users are authorized for anything else
446 # Admin users are authorized for anything else
447 return true if admin?
447 return true if admin?
448
448
449 roles = roles_for_project(context)
449 roles = roles_for_project(context)
450 return false unless roles
450 return false unless roles
451 roles.detect {|role|
451 roles.detect {|role|
452 (context.is_public? || role.member?) &&
452 (context.is_public? || role.member?) &&
453 role.allowed_to?(action) &&
453 role.allowed_to?(action) &&
454 (block_given? ? yield(role, self) : true)
454 (block_given? ? yield(role, self) : true)
455 }
455 }
456 elsif context && context.is_a?(Array)
456 elsif context && context.is_a?(Array)
457 # Authorize if user is authorized on every element of the array
457 # Authorize if user is authorized on every element of the array
458 context.map do |project|
458 context.map do |project|
459 allowed_to?(action, project, options, &block)
459 allowed_to?(action, project, options, &block)
460 end.inject do |memo,allowed|
460 end.inject do |memo,allowed|
461 memo && allowed
461 memo && allowed
462 end
462 end
463 elsif options[:global]
463 elsif options[:global]
464 # Admin users are always authorized
464 # Admin users are always authorized
465 return true if admin?
465 return true if admin?
466
466
467 # authorize if user has at least one role that has this permission
467 # authorize if user has at least one role that has this permission
468 roles = memberships.collect {|m| m.roles}.flatten.uniq
468 roles = memberships.collect {|m| m.roles}.flatten.uniq
469 roles << (self.logged? ? Role.non_member : Role.anonymous)
469 roles << (self.logged? ? Role.non_member : Role.anonymous)
470 roles.detect {|role|
470 roles.detect {|role|
471 role.allowed_to?(action) &&
471 role.allowed_to?(action) &&
472 (block_given? ? yield(role, self) : true)
472 (block_given? ? yield(role, self) : true)
473 }
473 }
474 else
474 else
475 false
475 false
476 end
476 end
477 end
477 end
478
478
479 # Is the user allowed to do the specified action on any project?
479 # Is the user allowed to do the specified action on any project?
480 # See allowed_to? for the actions and valid options.
480 # See allowed_to? for the actions and valid options.
481 def allowed_to_globally?(action, options, &block)
481 def allowed_to_globally?(action, options, &block)
482 allowed_to?(action, nil, options.reverse_merge(:global => true), &block)
482 allowed_to?(action, nil, options.reverse_merge(:global => true), &block)
483 end
483 end
484
484
485 safe_attributes 'login',
485 safe_attributes 'login',
486 'firstname',
486 'firstname',
487 'lastname',
487 'lastname',
488 'mail',
488 'mail',
489 'mail_notification',
489 'mail_notification',
490 'language',
490 'language',
491 'custom_field_values',
491 'custom_field_values',
492 'custom_fields',
492 'custom_fields',
493 'identity_url'
493 'identity_url'
494
494
495 safe_attributes 'status',
495 safe_attributes 'status',
496 'auth_source_id',
496 'auth_source_id',
497 :if => lambda {|user, current_user| current_user.admin?}
497 :if => lambda {|user, current_user| current_user.admin?}
498
498
499 safe_attributes 'group_ids',
499 safe_attributes 'group_ids',
500 :if => lambda {|user, current_user| current_user.admin? && !user.new_record?}
500 :if => lambda {|user, current_user| current_user.admin? && !user.new_record?}
501
501
502 # Utility method to help check if a user should be notified about an
502 # Utility method to help check if a user should be notified about an
503 # event.
503 # event.
504 #
504 #
505 # TODO: only supports Issue events currently
505 # TODO: only supports Issue events currently
506 def notify_about?(object)
506 def notify_about?(object)
507 case mail_notification
507 case mail_notification
508 when 'all'
508 when 'all'
509 true
509 true
510 when 'selected'
510 when 'selected'
511 # user receives notifications for created/assigned issues on unselected projects
511 # user receives notifications for created/assigned issues on unselected projects
512 if object.is_a?(Issue) && (object.author == self || is_or_belongs_to?(object.assigned_to))
512 if object.is_a?(Issue) && (object.author == self || is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was))
513 true
513 true
514 else
514 else
515 false
515 false
516 end
516 end
517 when 'none'
517 when 'none'
518 false
518 false
519 when 'only_my_events'
519 when 'only_my_events'
520 if object.is_a?(Issue) && (object.author == self || is_or_belongs_to?(object.assigned_to))
520 if object.is_a?(Issue) && (object.author == self || is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was))
521 true
521 true
522 else
522 else
523 false
523 false
524 end
524 end
525 when 'only_assigned'
525 when 'only_assigned'
526 if object.is_a?(Issue) && is_or_belongs_to?(object.assigned_to)
526 if object.is_a?(Issue) && (is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was))
527 true
527 true
528 else
528 else
529 false
529 false
530 end
530 end
531 when 'only_owner'
531 when 'only_owner'
532 if object.is_a?(Issue) && object.author == self
532 if object.is_a?(Issue) && object.author == self
533 true
533 true
534 else
534 else
535 false
535 false
536 end
536 end
537 else
537 else
538 false
538 false
539 end
539 end
540 end
540 end
541
541
542 def self.current=(user)
542 def self.current=(user)
543 @current_user = user
543 @current_user = user
544 end
544 end
545
545
546 def self.current
546 def self.current
547 @current_user ||= User.anonymous
547 @current_user ||= User.anonymous
548 end
548 end
549
549
550 # Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only
550 # Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only
551 # one anonymous user per database.
551 # one anonymous user per database.
552 def self.anonymous
552 def self.anonymous
553 anonymous_user = AnonymousUser.find(:first)
553 anonymous_user = AnonymousUser.find(:first)
554 if anonymous_user.nil?
554 if anonymous_user.nil?
555 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
555 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
556 raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
556 raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
557 end
557 end
558 anonymous_user
558 anonymous_user
559 end
559 end
560
560
561 # Salts all existing unsalted passwords
561 # Salts all existing unsalted passwords
562 # It changes password storage scheme from SHA1(password) to SHA1(salt + SHA1(password))
562 # It changes password storage scheme from SHA1(password) to SHA1(salt + SHA1(password))
563 # This method is used in the SaltPasswords migration and is to be kept as is
563 # This method is used in the SaltPasswords migration and is to be kept as is
564 def self.salt_unsalted_passwords!
564 def self.salt_unsalted_passwords!
565 transaction do
565 transaction do
566 User.find_each(:conditions => "salt IS NULL OR salt = ''") do |user|
566 User.find_each(:conditions => "salt IS NULL OR salt = ''") do |user|
567 next if user.hashed_password.blank?
567 next if user.hashed_password.blank?
568 salt = User.generate_salt
568 salt = User.generate_salt
569 hashed_password = User.hash_password("#{salt}#{user.hashed_password}")
569 hashed_password = User.hash_password("#{salt}#{user.hashed_password}")
570 User.update_all("salt = '#{salt}', hashed_password = '#{hashed_password}'", ["id = ?", user.id] )
570 User.update_all("salt = '#{salt}', hashed_password = '#{hashed_password}'", ["id = ?", user.id] )
571 end
571 end
572 end
572 end
573 end
573 end
574
574
575 protected
575 protected
576
576
577 def validate_password_length
577 def validate_password_length
578 # Password length validation based on setting
578 # Password length validation based on setting
579 if !password.nil? && password.size < Setting.password_min_length.to_i
579 if !password.nil? && password.size < Setting.password_min_length.to_i
580 errors.add(:password, :too_short, :count => Setting.password_min_length.to_i)
580 errors.add(:password, :too_short, :count => Setting.password_min_length.to_i)
581 end
581 end
582 end
582 end
583
583
584 private
584 private
585
585
586 # Removes references that are not handled by associations
586 # Removes references that are not handled by associations
587 # Things that are not deleted are reassociated with the anonymous user
587 # Things that are not deleted are reassociated with the anonymous user
588 def remove_references_before_destroy
588 def remove_references_before_destroy
589 return if self.id.nil?
589 return if self.id.nil?
590
590
591 substitute = User.anonymous
591 substitute = User.anonymous
592 Attachment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
592 Attachment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
593 Comment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
593 Comment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
594 Issue.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
594 Issue.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
595 Issue.update_all 'assigned_to_id = NULL', ['assigned_to_id = ?', id]
595 Issue.update_all 'assigned_to_id = NULL', ['assigned_to_id = ?', id]
596 Journal.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
596 Journal.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
597 JournalDetail.update_all ['old_value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND old_value = ?", id.to_s]
597 JournalDetail.update_all ['old_value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND old_value = ?", id.to_s]
598 JournalDetail.update_all ['value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND value = ?", id.to_s]
598 JournalDetail.update_all ['value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND value = ?", id.to_s]
599 Message.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
599 Message.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
600 News.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
600 News.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
601 # Remove private queries and keep public ones
601 # Remove private queries and keep public ones
602 ::Query.delete_all ['user_id = ? AND is_public = ?', id, false]
602 ::Query.delete_all ['user_id = ? AND is_public = ?', id, false]
603 ::Query.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
603 ::Query.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
604 TimeEntry.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
604 TimeEntry.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
605 Token.delete_all ['user_id = ?', id]
605 Token.delete_all ['user_id = ?', id]
606 Watcher.delete_all ['user_id = ?', id]
606 Watcher.delete_all ['user_id = ?', id]
607 WikiContent.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
607 WikiContent.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
608 WikiContent::Version.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
608 WikiContent::Version.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
609 end
609 end
610
610
611 # Return password digest
611 # Return password digest
612 def self.hash_password(clear_password)
612 def self.hash_password(clear_password)
613 Digest::SHA1.hexdigest(clear_password || "")
613 Digest::SHA1.hexdigest(clear_password || "")
614 end
614 end
615
615
616 # Returns a 128bits random salt as a hex string (32 chars long)
616 # Returns a 128bits random salt as a hex string (32 chars long)
617 def self.generate_salt
617 def self.generate_salt
618 ActiveSupport::SecureRandom.hex(16)
618 ActiveSupport::SecureRandom.hex(16)
619 end
619 end
620
620
621 end
621 end
622
622
623 class AnonymousUser < User
623 class AnonymousUser < User
624 validate :validate_anonymous_uniqueness, :on => :create
624 validate :validate_anonymous_uniqueness, :on => :create
625
625
626 def validate_anonymous_uniqueness
626 def validate_anonymous_uniqueness
627 # There should be only one AnonymousUser in the database
627 # There should be only one AnonymousUser in the database
628 errors.add :base, 'An anonymous user already exists.' if AnonymousUser.find(:first)
628 errors.add :base, 'An anonymous user already exists.' if AnonymousUser.find(:first)
629 end
629 end
630
630
631 def available_custom_fields
631 def available_custom_fields
632 []
632 []
633 end
633 end
634
634
635 # Overrides a few properties
635 # Overrides a few properties
636 def logged?; false end
636 def logged?; false end
637 def admin; false end
637 def admin; false end
638 def name(*args); I18n.t(:label_user_anonymous) end
638 def name(*args); I18n.t(:label_user_anonymous) end
639 def mail; nil end
639 def mail; nil end
640 def time_zone; nil end
640 def time_zone; nil end
641 def rss_key; nil end
641 def rss_key; nil end
642
642
643 # Anonymous user can not be destroyed
643 # Anonymous user can not be destroyed
644 def destroy
644 def destroy
645 false
645 false
646 end
646 end
647 end
647 end
@@ -1,1192 +1,1204
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19
19
20 class IssueTest < ActiveSupport::TestCase
20 class IssueTest < ActiveSupport::TestCase
21 fixtures :projects, :users, :members, :member_roles, :roles,
21 fixtures :projects, :users, :members, :member_roles, :roles,
22 :groups_users,
22 :groups_users,
23 :trackers, :projects_trackers,
23 :trackers, :projects_trackers,
24 :enabled_modules,
24 :enabled_modules,
25 :versions,
25 :versions,
26 :issue_statuses, :issue_categories, :issue_relations, :workflows,
26 :issue_statuses, :issue_categories, :issue_relations, :workflows,
27 :enumerations,
27 :enumerations,
28 :issues,
28 :issues,
29 :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values,
29 :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values,
30 :time_entries
30 :time_entries
31
31
32 def test_create
32 def test_create
33 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
33 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
34 :status_id => 1, :priority => IssuePriority.all.first,
34 :status_id => 1, :priority => IssuePriority.all.first,
35 :subject => 'test_create',
35 :subject => 'test_create',
36 :description => 'IssueTest#test_create', :estimated_hours => '1:30')
36 :description => 'IssueTest#test_create', :estimated_hours => '1:30')
37 assert issue.save
37 assert issue.save
38 issue.reload
38 issue.reload
39 assert_equal 1.5, issue.estimated_hours
39 assert_equal 1.5, issue.estimated_hours
40 end
40 end
41
41
42 def test_create_minimal
42 def test_create_minimal
43 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
43 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
44 :status_id => 1, :priority => IssuePriority.all.first,
44 :status_id => 1, :priority => IssuePriority.all.first,
45 :subject => 'test_create')
45 :subject => 'test_create')
46 assert issue.save
46 assert issue.save
47 assert issue.description.nil?
47 assert issue.description.nil?
48 end
48 end
49
49
50 def test_create_with_required_custom_field
50 def test_create_with_required_custom_field
51 field = IssueCustomField.find_by_name('Database')
51 field = IssueCustomField.find_by_name('Database')
52 field.update_attribute(:is_required, true)
52 field.update_attribute(:is_required, true)
53
53
54 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
54 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
55 :status_id => 1, :subject => 'test_create',
55 :status_id => 1, :subject => 'test_create',
56 :description => 'IssueTest#test_create_with_required_custom_field')
56 :description => 'IssueTest#test_create_with_required_custom_field')
57 assert issue.available_custom_fields.include?(field)
57 assert issue.available_custom_fields.include?(field)
58 # No value for the custom field
58 # No value for the custom field
59 assert !issue.save
59 assert !issue.save
60 assert_equal I18n.translate('activerecord.errors.messages.invalid'),
60 assert_equal I18n.translate('activerecord.errors.messages.invalid'),
61 issue.errors[:custom_values].to_s
61 issue.errors[:custom_values].to_s
62 # Blank value
62 # Blank value
63 issue.custom_field_values = { field.id => '' }
63 issue.custom_field_values = { field.id => '' }
64 assert !issue.save
64 assert !issue.save
65 assert_equal I18n.translate('activerecord.errors.messages.invalid'),
65 assert_equal I18n.translate('activerecord.errors.messages.invalid'),
66 issue.errors[:custom_values].to_s
66 issue.errors[:custom_values].to_s
67 # Invalid value
67 # Invalid value
68 issue.custom_field_values = { field.id => 'SQLServer' }
68 issue.custom_field_values = { field.id => 'SQLServer' }
69 assert !issue.save
69 assert !issue.save
70 assert_equal I18n.translate('activerecord.errors.messages.invalid'),
70 assert_equal I18n.translate('activerecord.errors.messages.invalid'),
71 issue.errors[:custom_values].to_s
71 issue.errors[:custom_values].to_s
72 # Valid value
72 # Valid value
73 issue.custom_field_values = { field.id => 'PostgreSQL' }
73 issue.custom_field_values = { field.id => 'PostgreSQL' }
74 assert issue.save
74 assert issue.save
75 issue.reload
75 issue.reload
76 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
76 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
77 end
77 end
78
78
79 def test_create_with_group_assignment
79 def test_create_with_group_assignment
80 with_settings :issue_group_assignment => '1' do
80 with_settings :issue_group_assignment => '1' do
81 assert Issue.new(:project_id => 2, :tracker_id => 1, :author_id => 1,
81 assert Issue.new(:project_id => 2, :tracker_id => 1, :author_id => 1,
82 :subject => 'Group assignment',
82 :subject => 'Group assignment',
83 :assigned_to_id => 11).save
83 :assigned_to_id => 11).save
84 issue = Issue.first(:order => 'id DESC')
84 issue = Issue.first(:order => 'id DESC')
85 assert_kind_of Group, issue.assigned_to
85 assert_kind_of Group, issue.assigned_to
86 assert_equal Group.find(11), issue.assigned_to
86 assert_equal Group.find(11), issue.assigned_to
87 end
87 end
88 end
88 end
89
89
90 def assert_visibility_match(user, issues)
90 def assert_visibility_match(user, issues)
91 assert_equal issues.collect(&:id).sort, Issue.all.select {|issue| issue.visible?(user)}.collect(&:id).sort
91 assert_equal issues.collect(&:id).sort, Issue.all.select {|issue| issue.visible?(user)}.collect(&:id).sort
92 end
92 end
93
93
94 def test_visible_scope_for_anonymous
94 def test_visible_scope_for_anonymous
95 # Anonymous user should see issues of public projects only
95 # Anonymous user should see issues of public projects only
96 issues = Issue.visible(User.anonymous).all
96 issues = Issue.visible(User.anonymous).all
97 assert issues.any?
97 assert issues.any?
98 assert_nil issues.detect {|issue| !issue.project.is_public?}
98 assert_nil issues.detect {|issue| !issue.project.is_public?}
99 assert_nil issues.detect {|issue| issue.is_private?}
99 assert_nil issues.detect {|issue| issue.is_private?}
100 assert_visibility_match User.anonymous, issues
100 assert_visibility_match User.anonymous, issues
101 end
101 end
102
102
103 def test_visible_scope_for_anonymous_with_own_issues_visibility
103 def test_visible_scope_for_anonymous_with_own_issues_visibility
104 Role.anonymous.update_attribute :issues_visibility, 'own'
104 Role.anonymous.update_attribute :issues_visibility, 'own'
105 Issue.create!(:project_id => 1, :tracker_id => 1,
105 Issue.create!(:project_id => 1, :tracker_id => 1,
106 :author_id => User.anonymous.id,
106 :author_id => User.anonymous.id,
107 :subject => 'Issue by anonymous')
107 :subject => 'Issue by anonymous')
108
108
109 issues = Issue.visible(User.anonymous).all
109 issues = Issue.visible(User.anonymous).all
110 assert issues.any?
110 assert issues.any?
111 assert_nil issues.detect {|issue| issue.author != User.anonymous}
111 assert_nil issues.detect {|issue| issue.author != User.anonymous}
112 assert_visibility_match User.anonymous, issues
112 assert_visibility_match User.anonymous, issues
113 end
113 end
114
114
115 def test_visible_scope_for_anonymous_without_view_issues_permissions
115 def test_visible_scope_for_anonymous_without_view_issues_permissions
116 # Anonymous user should not see issues without permission
116 # Anonymous user should not see issues without permission
117 Role.anonymous.remove_permission!(:view_issues)
117 Role.anonymous.remove_permission!(:view_issues)
118 issues = Issue.visible(User.anonymous).all
118 issues = Issue.visible(User.anonymous).all
119 assert issues.empty?
119 assert issues.empty?
120 assert_visibility_match User.anonymous, issues
120 assert_visibility_match User.anonymous, issues
121 end
121 end
122
122
123 def test_visible_scope_for_non_member
123 def test_visible_scope_for_non_member
124 user = User.find(9)
124 user = User.find(9)
125 assert user.projects.empty?
125 assert user.projects.empty?
126 # Non member user should see issues of public projects only
126 # Non member user should see issues of public projects only
127 issues = Issue.visible(user).all
127 issues = Issue.visible(user).all
128 assert issues.any?
128 assert issues.any?
129 assert_nil issues.detect {|issue| !issue.project.is_public?}
129 assert_nil issues.detect {|issue| !issue.project.is_public?}
130 assert_nil issues.detect {|issue| issue.is_private?}
130 assert_nil issues.detect {|issue| issue.is_private?}
131 assert_visibility_match user, issues
131 assert_visibility_match user, issues
132 end
132 end
133
133
134 def test_visible_scope_for_non_member_with_own_issues_visibility
134 def test_visible_scope_for_non_member_with_own_issues_visibility
135 Role.non_member.update_attribute :issues_visibility, 'own'
135 Role.non_member.update_attribute :issues_visibility, 'own'
136 Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 9, :subject => 'Issue by non member')
136 Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 9, :subject => 'Issue by non member')
137 user = User.find(9)
137 user = User.find(9)
138
138
139 issues = Issue.visible(user).all
139 issues = Issue.visible(user).all
140 assert issues.any?
140 assert issues.any?
141 assert_nil issues.detect {|issue| issue.author != user}
141 assert_nil issues.detect {|issue| issue.author != user}
142 assert_visibility_match user, issues
142 assert_visibility_match user, issues
143 end
143 end
144
144
145 def test_visible_scope_for_non_member_without_view_issues_permissions
145 def test_visible_scope_for_non_member_without_view_issues_permissions
146 # Non member user should not see issues without permission
146 # Non member user should not see issues without permission
147 Role.non_member.remove_permission!(:view_issues)
147 Role.non_member.remove_permission!(:view_issues)
148 user = User.find(9)
148 user = User.find(9)
149 assert user.projects.empty?
149 assert user.projects.empty?
150 issues = Issue.visible(user).all
150 issues = Issue.visible(user).all
151 assert issues.empty?
151 assert issues.empty?
152 assert_visibility_match user, issues
152 assert_visibility_match user, issues
153 end
153 end
154
154
155 def test_visible_scope_for_member
155 def test_visible_scope_for_member
156 user = User.find(9)
156 user = User.find(9)
157 # User should see issues of projects for which he has view_issues permissions only
157 # User should see issues of projects for which he has view_issues permissions only
158 Role.non_member.remove_permission!(:view_issues)
158 Role.non_member.remove_permission!(:view_issues)
159 Member.create!(:principal => user, :project_id => 3, :role_ids => [2])
159 Member.create!(:principal => user, :project_id => 3, :role_ids => [2])
160 issues = Issue.visible(user).all
160 issues = Issue.visible(user).all
161 assert issues.any?
161 assert issues.any?
162 assert_nil issues.detect {|issue| issue.project_id != 3}
162 assert_nil issues.detect {|issue| issue.project_id != 3}
163 assert_nil issues.detect {|issue| issue.is_private?}
163 assert_nil issues.detect {|issue| issue.is_private?}
164 assert_visibility_match user, issues
164 assert_visibility_match user, issues
165 end
165 end
166
166
167 def test_visible_scope_for_member_with_groups_should_return_assigned_issues
167 def test_visible_scope_for_member_with_groups_should_return_assigned_issues
168 user = User.find(8)
168 user = User.find(8)
169 assert user.groups.any?
169 assert user.groups.any?
170 Member.create!(:principal => user.groups.first, :project_id => 1, :role_ids => [2])
170 Member.create!(:principal => user.groups.first, :project_id => 1, :role_ids => [2])
171 Role.non_member.remove_permission!(:view_issues)
171 Role.non_member.remove_permission!(:view_issues)
172
172
173 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3,
173 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3,
174 :status_id => 1, :priority => IssuePriority.all.first,
174 :status_id => 1, :priority => IssuePriority.all.first,
175 :subject => 'Assignment test',
175 :subject => 'Assignment test',
176 :assigned_to => user.groups.first,
176 :assigned_to => user.groups.first,
177 :is_private => true)
177 :is_private => true)
178
178
179 Role.find(2).update_attribute :issues_visibility, 'default'
179 Role.find(2).update_attribute :issues_visibility, 'default'
180 issues = Issue.visible(User.find(8)).all
180 issues = Issue.visible(User.find(8)).all
181 assert issues.any?
181 assert issues.any?
182 assert issues.include?(issue)
182 assert issues.include?(issue)
183
183
184 Role.find(2).update_attribute :issues_visibility, 'own'
184 Role.find(2).update_attribute :issues_visibility, 'own'
185 issues = Issue.visible(User.find(8)).all
185 issues = Issue.visible(User.find(8)).all
186 assert issues.any?
186 assert issues.any?
187 assert issues.include?(issue)
187 assert issues.include?(issue)
188 end
188 end
189
189
190 def test_visible_scope_for_admin
190 def test_visible_scope_for_admin
191 user = User.find(1)
191 user = User.find(1)
192 user.members.each(&:destroy)
192 user.members.each(&:destroy)
193 assert user.projects.empty?
193 assert user.projects.empty?
194 issues = Issue.visible(user).all
194 issues = Issue.visible(user).all
195 assert issues.any?
195 assert issues.any?
196 # Admin should see issues on private projects that he does not belong to
196 # Admin should see issues on private projects that he does not belong to
197 assert issues.detect {|issue| !issue.project.is_public?}
197 assert issues.detect {|issue| !issue.project.is_public?}
198 # Admin should see private issues of other users
198 # Admin should see private issues of other users
199 assert issues.detect {|issue| issue.is_private? && issue.author != user}
199 assert issues.detect {|issue| issue.is_private? && issue.author != user}
200 assert_visibility_match user, issues
200 assert_visibility_match user, issues
201 end
201 end
202
202
203 def test_visible_scope_with_project
203 def test_visible_scope_with_project
204 project = Project.find(1)
204 project = Project.find(1)
205 issues = Issue.visible(User.find(2), :project => project).all
205 issues = Issue.visible(User.find(2), :project => project).all
206 projects = issues.collect(&:project).uniq
206 projects = issues.collect(&:project).uniq
207 assert_equal 1, projects.size
207 assert_equal 1, projects.size
208 assert_equal project, projects.first
208 assert_equal project, projects.first
209 end
209 end
210
210
211 def test_visible_scope_with_project_and_subprojects
211 def test_visible_scope_with_project_and_subprojects
212 project = Project.find(1)
212 project = Project.find(1)
213 issues = Issue.visible(User.find(2), :project => project, :with_subprojects => true).all
213 issues = Issue.visible(User.find(2), :project => project, :with_subprojects => true).all
214 projects = issues.collect(&:project).uniq
214 projects = issues.collect(&:project).uniq
215 assert projects.size > 1
215 assert projects.size > 1
216 assert_equal [], projects.select {|p| !p.is_or_is_descendant_of?(project)}
216 assert_equal [], projects.select {|p| !p.is_or_is_descendant_of?(project)}
217 end
217 end
218
218
219 def test_visible_and_nested_set_scopes
219 def test_visible_and_nested_set_scopes
220 assert_equal 0, Issue.find(1).descendants.visible.all.size
220 assert_equal 0, Issue.find(1).descendants.visible.all.size
221 end
221 end
222
222
223 def test_open_scope
223 def test_open_scope
224 issues = Issue.open.all
224 issues = Issue.open.all
225 assert_nil issues.detect(&:closed?)
225 assert_nil issues.detect(&:closed?)
226 end
226 end
227
227
228 def test_open_scope_with_arg
228 def test_open_scope_with_arg
229 issues = Issue.open(false).all
229 issues = Issue.open(false).all
230 assert_equal issues, issues.select(&:closed?)
230 assert_equal issues, issues.select(&:closed?)
231 end
231 end
232
232
233 def test_errors_full_messages_should_include_custom_fields_errors
233 def test_errors_full_messages_should_include_custom_fields_errors
234 field = IssueCustomField.find_by_name('Database')
234 field = IssueCustomField.find_by_name('Database')
235
235
236 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
236 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
237 :status_id => 1, :subject => 'test_create',
237 :status_id => 1, :subject => 'test_create',
238 :description => 'IssueTest#test_create_with_required_custom_field')
238 :description => 'IssueTest#test_create_with_required_custom_field')
239 assert issue.available_custom_fields.include?(field)
239 assert issue.available_custom_fields.include?(field)
240 # Invalid value
240 # Invalid value
241 issue.custom_field_values = { field.id => 'SQLServer' }
241 issue.custom_field_values = { field.id => 'SQLServer' }
242
242
243 assert !issue.valid?
243 assert !issue.valid?
244 assert_equal 1, issue.errors.full_messages.size
244 assert_equal 1, issue.errors.full_messages.size
245 assert_equal "Database #{I18n.translate('activerecord.errors.messages.inclusion')}",
245 assert_equal "Database #{I18n.translate('activerecord.errors.messages.inclusion')}",
246 issue.errors.full_messages.first
246 issue.errors.full_messages.first
247 end
247 end
248
248
249 def test_update_issue_with_required_custom_field
249 def test_update_issue_with_required_custom_field
250 field = IssueCustomField.find_by_name('Database')
250 field = IssueCustomField.find_by_name('Database')
251 field.update_attribute(:is_required, true)
251 field.update_attribute(:is_required, true)
252
252
253 issue = Issue.find(1)
253 issue = Issue.find(1)
254 assert_nil issue.custom_value_for(field)
254 assert_nil issue.custom_value_for(field)
255 assert issue.available_custom_fields.include?(field)
255 assert issue.available_custom_fields.include?(field)
256 # No change to custom values, issue can be saved
256 # No change to custom values, issue can be saved
257 assert issue.save
257 assert issue.save
258 # Blank value
258 # Blank value
259 issue.custom_field_values = { field.id => '' }
259 issue.custom_field_values = { field.id => '' }
260 assert !issue.save
260 assert !issue.save
261 # Valid value
261 # Valid value
262 issue.custom_field_values = { field.id => 'PostgreSQL' }
262 issue.custom_field_values = { field.id => 'PostgreSQL' }
263 assert issue.save
263 assert issue.save
264 issue.reload
264 issue.reload
265 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
265 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
266 end
266 end
267
267
268 def test_should_not_update_attributes_if_custom_fields_validation_fails
268 def test_should_not_update_attributes_if_custom_fields_validation_fails
269 issue = Issue.find(1)
269 issue = Issue.find(1)
270 field = IssueCustomField.find_by_name('Database')
270 field = IssueCustomField.find_by_name('Database')
271 assert issue.available_custom_fields.include?(field)
271 assert issue.available_custom_fields.include?(field)
272
272
273 issue.custom_field_values = { field.id => 'Invalid' }
273 issue.custom_field_values = { field.id => 'Invalid' }
274 issue.subject = 'Should be not be saved'
274 issue.subject = 'Should be not be saved'
275 assert !issue.save
275 assert !issue.save
276
276
277 issue.reload
277 issue.reload
278 assert_equal "Can't print recipes", issue.subject
278 assert_equal "Can't print recipes", issue.subject
279 end
279 end
280
280
281 def test_should_not_recreate_custom_values_objects_on_update
281 def test_should_not_recreate_custom_values_objects_on_update
282 field = IssueCustomField.find_by_name('Database')
282 field = IssueCustomField.find_by_name('Database')
283
283
284 issue = Issue.find(1)
284 issue = Issue.find(1)
285 issue.custom_field_values = { field.id => 'PostgreSQL' }
285 issue.custom_field_values = { field.id => 'PostgreSQL' }
286 assert issue.save
286 assert issue.save
287 custom_value = issue.custom_value_for(field)
287 custom_value = issue.custom_value_for(field)
288 issue.reload
288 issue.reload
289 issue.custom_field_values = { field.id => 'MySQL' }
289 issue.custom_field_values = { field.id => 'MySQL' }
290 assert issue.save
290 assert issue.save
291 issue.reload
291 issue.reload
292 assert_equal custom_value.id, issue.custom_value_for(field).id
292 assert_equal custom_value.id, issue.custom_value_for(field).id
293 end
293 end
294
294
295 def test_should_not_update_custom_fields_on_changing_tracker_with_different_custom_fields
295 def test_should_not_update_custom_fields_on_changing_tracker_with_different_custom_fields
296 issue = Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => 'Test', :custom_field_values => {'2' => 'Test'})
296 issue = Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => 'Test', :custom_field_values => {'2' => 'Test'})
297 assert !Tracker.find(2).custom_field_ids.include?(2)
297 assert !Tracker.find(2).custom_field_ids.include?(2)
298
298
299 issue = Issue.find(issue.id)
299 issue = Issue.find(issue.id)
300 issue.attributes = {:tracker_id => 2, :custom_field_values => {'1' => ''}}
300 issue.attributes = {:tracker_id => 2, :custom_field_values => {'1' => ''}}
301
301
302 issue = Issue.find(issue.id)
302 issue = Issue.find(issue.id)
303 custom_value = issue.custom_value_for(2)
303 custom_value = issue.custom_value_for(2)
304 assert_not_nil custom_value
304 assert_not_nil custom_value
305 assert_equal 'Test', custom_value.value
305 assert_equal 'Test', custom_value.value
306 end
306 end
307
307
308 def test_assigning_tracker_id_should_reload_custom_fields_values
308 def test_assigning_tracker_id_should_reload_custom_fields_values
309 issue = Issue.new(:project => Project.find(1))
309 issue = Issue.new(:project => Project.find(1))
310 assert issue.custom_field_values.empty?
310 assert issue.custom_field_values.empty?
311 issue.tracker_id = 1
311 issue.tracker_id = 1
312 assert issue.custom_field_values.any?
312 assert issue.custom_field_values.any?
313 end
313 end
314
314
315 def test_assigning_attributes_should_assign_project_and_tracker_first
315 def test_assigning_attributes_should_assign_project_and_tracker_first
316 seq = sequence('seq')
316 seq = sequence('seq')
317 issue = Issue.new
317 issue = Issue.new
318 issue.expects(:project_id=).in_sequence(seq)
318 issue.expects(:project_id=).in_sequence(seq)
319 issue.expects(:tracker_id=).in_sequence(seq)
319 issue.expects(:tracker_id=).in_sequence(seq)
320 issue.expects(:subject=).in_sequence(seq)
320 issue.expects(:subject=).in_sequence(seq)
321 issue.attributes = {:tracker_id => 2, :project_id => 1, :subject => 'Test'}
321 issue.attributes = {:tracker_id => 2, :project_id => 1, :subject => 'Test'}
322 end
322 end
323
323
324 def test_assigning_tracker_and_custom_fields_should_assign_custom_fields
324 def test_assigning_tracker_and_custom_fields_should_assign_custom_fields
325 attributes = ActiveSupport::OrderedHash.new
325 attributes = ActiveSupport::OrderedHash.new
326 attributes['custom_field_values'] = { '1' => 'MySQL' }
326 attributes['custom_field_values'] = { '1' => 'MySQL' }
327 attributes['tracker_id'] = '1'
327 attributes['tracker_id'] = '1'
328 issue = Issue.new(:project => Project.find(1))
328 issue = Issue.new(:project => Project.find(1))
329 issue.attributes = attributes
329 issue.attributes = attributes
330 assert_not_nil issue.custom_value_for(1)
330 assert_not_nil issue.custom_value_for(1)
331 assert_equal 'MySQL', issue.custom_value_for(1).value
331 assert_equal 'MySQL', issue.custom_value_for(1).value
332 end
332 end
333
333
334 def test_should_update_issue_with_disabled_tracker
334 def test_should_update_issue_with_disabled_tracker
335 p = Project.find(1)
335 p = Project.find(1)
336 issue = Issue.find(1)
336 issue = Issue.find(1)
337
337
338 p.trackers.delete(issue.tracker)
338 p.trackers.delete(issue.tracker)
339 assert !p.trackers.include?(issue.tracker)
339 assert !p.trackers.include?(issue.tracker)
340
340
341 issue.reload
341 issue.reload
342 issue.subject = 'New subject'
342 issue.subject = 'New subject'
343 assert issue.save
343 assert issue.save
344 end
344 end
345
345
346 def test_should_not_set_a_disabled_tracker
346 def test_should_not_set_a_disabled_tracker
347 p = Project.find(1)
347 p = Project.find(1)
348 p.trackers.delete(Tracker.find(2))
348 p.trackers.delete(Tracker.find(2))
349
349
350 issue = Issue.find(1)
350 issue = Issue.find(1)
351 issue.tracker_id = 2
351 issue.tracker_id = 2
352 issue.subject = 'New subject'
352 issue.subject = 'New subject'
353 assert !issue.save
353 assert !issue.save
354 assert_not_nil issue.errors[:tracker_id]
354 assert_not_nil issue.errors[:tracker_id]
355 end
355 end
356
356
357 def test_category_based_assignment
357 def test_category_based_assignment
358 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3,
358 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3,
359 :status_id => 1, :priority => IssuePriority.all.first,
359 :status_id => 1, :priority => IssuePriority.all.first,
360 :subject => 'Assignment test',
360 :subject => 'Assignment test',
361 :description => 'Assignment test', :category_id => 1)
361 :description => 'Assignment test', :category_id => 1)
362 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
362 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
363 end
363 end
364
364
365 def test_new_statuses_allowed_to
365 def test_new_statuses_allowed_to
366 Workflow.delete_all
366 Workflow.delete_all
367
367
368 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 2, :author => false, :assignee => false)
368 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 2, :author => false, :assignee => false)
369 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 3, :author => true, :assignee => false)
369 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 3, :author => true, :assignee => false)
370 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 4, :author => false, :assignee => true)
370 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 4, :author => false, :assignee => true)
371 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 5, :author => true, :assignee => true)
371 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 5, :author => true, :assignee => true)
372 status = IssueStatus.find(1)
372 status = IssueStatus.find(1)
373 role = Role.find(1)
373 role = Role.find(1)
374 tracker = Tracker.find(1)
374 tracker = Tracker.find(1)
375 user = User.find(2)
375 user = User.find(2)
376
376
377 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1)
377 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1)
378 assert_equal [1, 2], issue.new_statuses_allowed_to(user).map(&:id)
378 assert_equal [1, 2], issue.new_statuses_allowed_to(user).map(&:id)
379
379
380 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1, :author => user)
380 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1, :author => user)
381 assert_equal [1, 2, 3, 5], issue.new_statuses_allowed_to(user).map(&:id)
381 assert_equal [1, 2, 3, 5], issue.new_statuses_allowed_to(user).map(&:id)
382
382
383 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1, :assigned_to => user)
383 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1, :assigned_to => user)
384 assert_equal [1, 2, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
384 assert_equal [1, 2, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
385
385
386 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1, :author => user, :assigned_to => user)
386 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1, :author => user, :assigned_to => user)
387 assert_equal [1, 2, 3, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
387 assert_equal [1, 2, 3, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
388 end
388 end
389
389
390 def test_copy
390 def test_copy
391 issue = Issue.new.copy_from(1)
391 issue = Issue.new.copy_from(1)
392 assert issue.copy?
392 assert issue.copy?
393 assert issue.save
393 assert issue.save
394 issue.reload
394 issue.reload
395 orig = Issue.find(1)
395 orig = Issue.find(1)
396 assert_equal orig.subject, issue.subject
396 assert_equal orig.subject, issue.subject
397 assert_equal orig.tracker, issue.tracker
397 assert_equal orig.tracker, issue.tracker
398 assert_equal "125", issue.custom_value_for(2).value
398 assert_equal "125", issue.custom_value_for(2).value
399 end
399 end
400
400
401 def test_copy_should_copy_status
401 def test_copy_should_copy_status
402 orig = Issue.find(8)
402 orig = Issue.find(8)
403 assert orig.status != IssueStatus.default
403 assert orig.status != IssueStatus.default
404
404
405 issue = Issue.new.copy_from(orig)
405 issue = Issue.new.copy_from(orig)
406 assert issue.save
406 assert issue.save
407 issue.reload
407 issue.reload
408 assert_equal orig.status, issue.status
408 assert_equal orig.status, issue.status
409 end
409 end
410
410
411 def test_should_not_call_after_project_change_on_creation
411 def test_should_not_call_after_project_change_on_creation
412 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1, :subject => 'Test', :author_id => 1)
412 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1, :subject => 'Test', :author_id => 1)
413 issue.expects(:after_project_change).never
413 issue.expects(:after_project_change).never
414 issue.save!
414 issue.save!
415 end
415 end
416
416
417 def test_should_not_call_after_project_change_on_update
417 def test_should_not_call_after_project_change_on_update
418 issue = Issue.find(1)
418 issue = Issue.find(1)
419 issue.project = Project.find(1)
419 issue.project = Project.find(1)
420 issue.subject = 'No project change'
420 issue.subject = 'No project change'
421 issue.expects(:after_project_change).never
421 issue.expects(:after_project_change).never
422 issue.save!
422 issue.save!
423 end
423 end
424
424
425 def test_should_call_after_project_change_on_project_change
425 def test_should_call_after_project_change_on_project_change
426 issue = Issue.find(1)
426 issue = Issue.find(1)
427 issue.project = Project.find(2)
427 issue.project = Project.find(2)
428 issue.expects(:after_project_change).once
428 issue.expects(:after_project_change).once
429 issue.save!
429 issue.save!
430 end
430 end
431
431
432 def test_should_close_duplicates
432 def test_should_close_duplicates
433 # Create 3 issues
433 # Create 3 issues
434 project = Project.find(1)
434 project = Project.find(1)
435 issue1 = Issue.generate_for_project!(project)
435 issue1 = Issue.generate_for_project!(project)
436 issue2 = Issue.generate_for_project!(project)
436 issue2 = Issue.generate_for_project!(project)
437 issue3 = Issue.generate_for_project!(project)
437 issue3 = Issue.generate_for_project!(project)
438
438
439 # 2 is a dupe of 1
439 # 2 is a dupe of 1
440 IssueRelation.create!(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
440 IssueRelation.create!(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
441 # And 3 is a dupe of 2
441 # And 3 is a dupe of 2
442 IssueRelation.create!(:issue_from => issue3, :issue_to => issue2, :relation_type => IssueRelation::TYPE_DUPLICATES)
442 IssueRelation.create!(:issue_from => issue3, :issue_to => issue2, :relation_type => IssueRelation::TYPE_DUPLICATES)
443 # And 3 is a dupe of 1 (circular duplicates)
443 # And 3 is a dupe of 1 (circular duplicates)
444 IssueRelation.create!(:issue_from => issue3, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
444 IssueRelation.create!(:issue_from => issue3, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
445
445
446 assert issue1.reload.duplicates.include?(issue2)
446 assert issue1.reload.duplicates.include?(issue2)
447
447
448 # Closing issue 1
448 # Closing issue 1
449 issue1.init_journal(User.find(:first), "Closing issue1")
449 issue1.init_journal(User.find(:first), "Closing issue1")
450 issue1.status = IssueStatus.find :first, :conditions => {:is_closed => true}
450 issue1.status = IssueStatus.find :first, :conditions => {:is_closed => true}
451 assert issue1.save
451 assert issue1.save
452 # 2 and 3 should be also closed
452 # 2 and 3 should be also closed
453 assert issue2.reload.closed?
453 assert issue2.reload.closed?
454 assert issue3.reload.closed?
454 assert issue3.reload.closed?
455 end
455 end
456
456
457 def test_should_not_close_duplicated_issue
457 def test_should_not_close_duplicated_issue
458 project = Project.find(1)
458 project = Project.find(1)
459 issue1 = Issue.generate_for_project!(project)
459 issue1 = Issue.generate_for_project!(project)
460 issue2 = Issue.generate_for_project!(project)
460 issue2 = Issue.generate_for_project!(project)
461
461
462 # 2 is a dupe of 1
462 # 2 is a dupe of 1
463 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
463 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
464 # 2 is a dup of 1 but 1 is not a duplicate of 2
464 # 2 is a dup of 1 but 1 is not a duplicate of 2
465 assert !issue2.reload.duplicates.include?(issue1)
465 assert !issue2.reload.duplicates.include?(issue1)
466
466
467 # Closing issue 2
467 # Closing issue 2
468 issue2.init_journal(User.find(:first), "Closing issue2")
468 issue2.init_journal(User.find(:first), "Closing issue2")
469 issue2.status = IssueStatus.find :first, :conditions => {:is_closed => true}
469 issue2.status = IssueStatus.find :first, :conditions => {:is_closed => true}
470 assert issue2.save
470 assert issue2.save
471 # 1 should not be also closed
471 # 1 should not be also closed
472 assert !issue1.reload.closed?
472 assert !issue1.reload.closed?
473 end
473 end
474
474
475 def test_assignable_versions
475 def test_assignable_versions
476 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
476 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
477 assert_equal ['open'], issue.assignable_versions.collect(&:status).uniq
477 assert_equal ['open'], issue.assignable_versions.collect(&:status).uniq
478 end
478 end
479
479
480 def test_should_not_be_able_to_assign_a_new_issue_to_a_closed_version
480 def test_should_not_be_able_to_assign_a_new_issue_to_a_closed_version
481 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
481 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
482 assert !issue.save
482 assert !issue.save
483 assert_not_nil issue.errors[:fixed_version_id]
483 assert_not_nil issue.errors[:fixed_version_id]
484 end
484 end
485
485
486 def test_should_not_be_able_to_assign_a_new_issue_to_a_locked_version
486 def test_should_not_be_able_to_assign_a_new_issue_to_a_locked_version
487 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 2, :subject => 'New issue')
487 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 2, :subject => 'New issue')
488 assert !issue.save
488 assert !issue.save
489 assert_not_nil issue.errors[:fixed_version_id]
489 assert_not_nil issue.errors[:fixed_version_id]
490 end
490 end
491
491
492 def test_should_be_able_to_assign_a_new_issue_to_an_open_version
492 def test_should_be_able_to_assign_a_new_issue_to_an_open_version
493 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 3, :subject => 'New issue')
493 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 3, :subject => 'New issue')
494 assert issue.save
494 assert issue.save
495 end
495 end
496
496
497 def test_should_be_able_to_update_an_issue_assigned_to_a_closed_version
497 def test_should_be_able_to_update_an_issue_assigned_to_a_closed_version
498 issue = Issue.find(11)
498 issue = Issue.find(11)
499 assert_equal 'closed', issue.fixed_version.status
499 assert_equal 'closed', issue.fixed_version.status
500 issue.subject = 'Subject changed'
500 issue.subject = 'Subject changed'
501 assert issue.save
501 assert issue.save
502 end
502 end
503
503
504 def test_should_not_be_able_to_reopen_an_issue_assigned_to_a_closed_version
504 def test_should_not_be_able_to_reopen_an_issue_assigned_to_a_closed_version
505 issue = Issue.find(11)
505 issue = Issue.find(11)
506 issue.status_id = 1
506 issue.status_id = 1
507 assert !issue.save
507 assert !issue.save
508 assert_not_nil issue.errors[:base]
508 assert_not_nil issue.errors[:base]
509 end
509 end
510
510
511 def test_should_be_able_to_reopen_and_reassign_an_issue_assigned_to_a_closed_version
511 def test_should_be_able_to_reopen_and_reassign_an_issue_assigned_to_a_closed_version
512 issue = Issue.find(11)
512 issue = Issue.find(11)
513 issue.status_id = 1
513 issue.status_id = 1
514 issue.fixed_version_id = 3
514 issue.fixed_version_id = 3
515 assert issue.save
515 assert issue.save
516 end
516 end
517
517
518 def test_should_be_able_to_reopen_an_issue_assigned_to_a_locked_version
518 def test_should_be_able_to_reopen_an_issue_assigned_to_a_locked_version
519 issue = Issue.find(12)
519 issue = Issue.find(12)
520 assert_equal 'locked', issue.fixed_version.status
520 assert_equal 'locked', issue.fixed_version.status
521 issue.status_id = 1
521 issue.status_id = 1
522 assert issue.save
522 assert issue.save
523 end
523 end
524
524
525 def test_move_to_another_project_with_same_category
525 def test_move_to_another_project_with_same_category
526 issue = Issue.find(1)
526 issue = Issue.find(1)
527 issue.project = Project.find(2)
527 issue.project = Project.find(2)
528 assert issue.save
528 assert issue.save
529 issue.reload
529 issue.reload
530 assert_equal 2, issue.project_id
530 assert_equal 2, issue.project_id
531 # Category changes
531 # Category changes
532 assert_equal 4, issue.category_id
532 assert_equal 4, issue.category_id
533 # Make sure time entries were move to the target project
533 # Make sure time entries were move to the target project
534 assert_equal 2, issue.time_entries.first.project_id
534 assert_equal 2, issue.time_entries.first.project_id
535 end
535 end
536
536
537 def test_move_to_another_project_without_same_category
537 def test_move_to_another_project_without_same_category
538 issue = Issue.find(2)
538 issue = Issue.find(2)
539 issue.project = Project.find(2)
539 issue.project = Project.find(2)
540 assert issue.save
540 assert issue.save
541 issue.reload
541 issue.reload
542 assert_equal 2, issue.project_id
542 assert_equal 2, issue.project_id
543 # Category cleared
543 # Category cleared
544 assert_nil issue.category_id
544 assert_nil issue.category_id
545 end
545 end
546
546
547 def test_move_to_another_project_should_clear_fixed_version_when_not_shared
547 def test_move_to_another_project_should_clear_fixed_version_when_not_shared
548 issue = Issue.find(1)
548 issue = Issue.find(1)
549 issue.update_attribute(:fixed_version_id, 1)
549 issue.update_attribute(:fixed_version_id, 1)
550 issue.project = Project.find(2)
550 issue.project = Project.find(2)
551 assert issue.save
551 assert issue.save
552 issue.reload
552 issue.reload
553 assert_equal 2, issue.project_id
553 assert_equal 2, issue.project_id
554 # Cleared fixed_version
554 # Cleared fixed_version
555 assert_equal nil, issue.fixed_version
555 assert_equal nil, issue.fixed_version
556 end
556 end
557
557
558 def test_move_to_another_project_should_keep_fixed_version_when_shared_with_the_target_project
558 def test_move_to_another_project_should_keep_fixed_version_when_shared_with_the_target_project
559 issue = Issue.find(1)
559 issue = Issue.find(1)
560 issue.update_attribute(:fixed_version_id, 4)
560 issue.update_attribute(:fixed_version_id, 4)
561 issue.project = Project.find(5)
561 issue.project = Project.find(5)
562 assert issue.save
562 assert issue.save
563 issue.reload
563 issue.reload
564 assert_equal 5, issue.project_id
564 assert_equal 5, issue.project_id
565 # Keep fixed_version
565 # Keep fixed_version
566 assert_equal 4, issue.fixed_version_id
566 assert_equal 4, issue.fixed_version_id
567 end
567 end
568
568
569 def test_move_to_another_project_should_clear_fixed_version_when_not_shared_with_the_target_project
569 def test_move_to_another_project_should_clear_fixed_version_when_not_shared_with_the_target_project
570 issue = Issue.find(1)
570 issue = Issue.find(1)
571 issue.update_attribute(:fixed_version_id, 1)
571 issue.update_attribute(:fixed_version_id, 1)
572 issue.project = Project.find(5)
572 issue.project = Project.find(5)
573 assert issue.save
573 assert issue.save
574 issue.reload
574 issue.reload
575 assert_equal 5, issue.project_id
575 assert_equal 5, issue.project_id
576 # Cleared fixed_version
576 # Cleared fixed_version
577 assert_equal nil, issue.fixed_version
577 assert_equal nil, issue.fixed_version
578 end
578 end
579
579
580 def test_move_to_another_project_should_keep_fixed_version_when_shared_systemwide
580 def test_move_to_another_project_should_keep_fixed_version_when_shared_systemwide
581 issue = Issue.find(1)
581 issue = Issue.find(1)
582 issue.update_attribute(:fixed_version_id, 7)
582 issue.update_attribute(:fixed_version_id, 7)
583 issue.project = Project.find(2)
583 issue.project = Project.find(2)
584 assert issue.save
584 assert issue.save
585 issue.reload
585 issue.reload
586 assert_equal 2, issue.project_id
586 assert_equal 2, issue.project_id
587 # Keep fixed_version
587 # Keep fixed_version
588 assert_equal 7, issue.fixed_version_id
588 assert_equal 7, issue.fixed_version_id
589 end
589 end
590
590
591 def test_move_to_another_project_with_disabled_tracker
591 def test_move_to_another_project_with_disabled_tracker
592 issue = Issue.find(1)
592 issue = Issue.find(1)
593 target = Project.find(2)
593 target = Project.find(2)
594 target.tracker_ids = [3]
594 target.tracker_ids = [3]
595 target.save
595 target.save
596 issue.project = target
596 issue.project = target
597 assert issue.save
597 assert issue.save
598 issue.reload
598 issue.reload
599 assert_equal 2, issue.project_id
599 assert_equal 2, issue.project_id
600 assert_equal 3, issue.tracker_id
600 assert_equal 3, issue.tracker_id
601 end
601 end
602
602
603 def test_copy_to_the_same_project
603 def test_copy_to_the_same_project
604 issue = Issue.find(1)
604 issue = Issue.find(1)
605 copy = issue.copy
605 copy = issue.copy
606 assert_difference 'Issue.count' do
606 assert_difference 'Issue.count' do
607 copy.save!
607 copy.save!
608 end
608 end
609 assert_kind_of Issue, copy
609 assert_kind_of Issue, copy
610 assert_equal issue.project, copy.project
610 assert_equal issue.project, copy.project
611 assert_equal "125", copy.custom_value_for(2).value
611 assert_equal "125", copy.custom_value_for(2).value
612 end
612 end
613
613
614 def test_copy_to_another_project_and_tracker
614 def test_copy_to_another_project_and_tracker
615 issue = Issue.find(1)
615 issue = Issue.find(1)
616 copy = issue.copy(:project_id => 3, :tracker_id => 2)
616 copy = issue.copy(:project_id => 3, :tracker_id => 2)
617 assert_difference 'Issue.count' do
617 assert_difference 'Issue.count' do
618 copy.save!
618 copy.save!
619 end
619 end
620 copy.reload
620 copy.reload
621 assert_kind_of Issue, copy
621 assert_kind_of Issue, copy
622 assert_equal Project.find(3), copy.project
622 assert_equal Project.find(3), copy.project
623 assert_equal Tracker.find(2), copy.tracker
623 assert_equal Tracker.find(2), copy.tracker
624 # Custom field #2 is not associated with target tracker
624 # Custom field #2 is not associated with target tracker
625 assert_nil copy.custom_value_for(2)
625 assert_nil copy.custom_value_for(2)
626 end
626 end
627
627
628 context "#copy" do
628 context "#copy" do
629 setup do
629 setup do
630 @issue = Issue.find(1)
630 @issue = Issue.find(1)
631 end
631 end
632
632
633 should "not create a journal" do
633 should "not create a journal" do
634 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :assigned_to_id => 3)
634 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :assigned_to_id => 3)
635 copy.save!
635 copy.save!
636 assert_equal 0, copy.reload.journals.size
636 assert_equal 0, copy.reload.journals.size
637 end
637 end
638
638
639 should "allow assigned_to changes" do
639 should "allow assigned_to changes" do
640 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :assigned_to_id => 3)
640 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :assigned_to_id => 3)
641 assert_equal 3, copy.assigned_to_id
641 assert_equal 3, copy.assigned_to_id
642 end
642 end
643
643
644 should "allow status changes" do
644 should "allow status changes" do
645 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :status_id => 2)
645 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :status_id => 2)
646 assert_equal 2, copy.status_id
646 assert_equal 2, copy.status_id
647 end
647 end
648
648
649 should "allow start date changes" do
649 should "allow start date changes" do
650 date = Date.today
650 date = Date.today
651 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :start_date => date)
651 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :start_date => date)
652 assert_equal date, copy.start_date
652 assert_equal date, copy.start_date
653 end
653 end
654
654
655 should "allow due date changes" do
655 should "allow due date changes" do
656 date = Date.today
656 date = Date.today
657 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :due_date => date)
657 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :due_date => date)
658 assert_equal date, copy.due_date
658 assert_equal date, copy.due_date
659 end
659 end
660
660
661 should "set current user as author" do
661 should "set current user as author" do
662 User.current = User.find(9)
662 User.current = User.find(9)
663 copy = @issue.copy(:project_id => 3, :tracker_id => 2)
663 copy = @issue.copy(:project_id => 3, :tracker_id => 2)
664 assert_equal User.current, copy.author
664 assert_equal User.current, copy.author
665 end
665 end
666
666
667 should "create a journal with notes" do
667 should "create a journal with notes" do
668 date = Date.today
668 date = Date.today
669 notes = "Notes added when copying"
669 notes = "Notes added when copying"
670 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :start_date => date)
670 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :start_date => date)
671 copy.init_journal(User.current, notes)
671 copy.init_journal(User.current, notes)
672 copy.save!
672 copy.save!
673
673
674 assert_equal 1, copy.journals.size
674 assert_equal 1, copy.journals.size
675 journal = copy.journals.first
675 journal = copy.journals.first
676 assert_equal 0, journal.details.size
676 assert_equal 0, journal.details.size
677 assert_equal notes, journal.notes
677 assert_equal notes, journal.notes
678 end
678 end
679 end
679 end
680
680
681 def test_recipients_should_include_previous_assignee
682 user = User.find(3)
683 user.members.update_all ["mail_notification = ?", false]
684 user.update_attribute :mail_notification, 'only_assigned'
685
686 issue = Issue.find(2)
687 issue.assigned_to = nil
688 assert_include user.mail, issue.recipients
689 issue.save!
690 assert !issue.recipients.include?(user.mail)
691 end
692
681 def test_recipients_should_not_include_users_that_cannot_view_the_issue
693 def test_recipients_should_not_include_users_that_cannot_view_the_issue
682 issue = Issue.find(12)
694 issue = Issue.find(12)
683 assert issue.recipients.include?(issue.author.mail)
695 assert issue.recipients.include?(issue.author.mail)
684 # copy the issue to a private project
696 # copy the issue to a private project
685 copy = issue.copy(:project_id => 5, :tracker_id => 2)
697 copy = issue.copy(:project_id => 5, :tracker_id => 2)
686 # author is not a member of project anymore
698 # author is not a member of project anymore
687 assert !copy.recipients.include?(copy.author.mail)
699 assert !copy.recipients.include?(copy.author.mail)
688 end
700 end
689
701
690 def test_recipients_should_include_the_assigned_group_members
702 def test_recipients_should_include_the_assigned_group_members
691 group_member = User.generate_with_protected!
703 group_member = User.generate_with_protected!
692 group = Group.generate!
704 group = Group.generate!
693 group.users << group_member
705 group.users << group_member
694
706
695 issue = Issue.find(12)
707 issue = Issue.find(12)
696 issue.assigned_to = group
708 issue.assigned_to = group
697 assert issue.recipients.include?(group_member.mail)
709 assert issue.recipients.include?(group_member.mail)
698 end
710 end
699
711
700 def test_watcher_recipients_should_not_include_users_that_cannot_view_the_issue
712 def test_watcher_recipients_should_not_include_users_that_cannot_view_the_issue
701 user = User.find(3)
713 user = User.find(3)
702 issue = Issue.find(9)
714 issue = Issue.find(9)
703 Watcher.create!(:user => user, :watchable => issue)
715 Watcher.create!(:user => user, :watchable => issue)
704 assert issue.watched_by?(user)
716 assert issue.watched_by?(user)
705 assert !issue.watcher_recipients.include?(user.mail)
717 assert !issue.watcher_recipients.include?(user.mail)
706 end
718 end
707
719
708 def test_issue_destroy
720 def test_issue_destroy
709 Issue.find(1).destroy
721 Issue.find(1).destroy
710 assert_nil Issue.find_by_id(1)
722 assert_nil Issue.find_by_id(1)
711 assert_nil TimeEntry.find_by_issue_id(1)
723 assert_nil TimeEntry.find_by_issue_id(1)
712 end
724 end
713
725
714 def test_blocked
726 def test_blocked
715 blocked_issue = Issue.find(9)
727 blocked_issue = Issue.find(9)
716 blocking_issue = Issue.find(10)
728 blocking_issue = Issue.find(10)
717
729
718 assert blocked_issue.blocked?
730 assert blocked_issue.blocked?
719 assert !blocking_issue.blocked?
731 assert !blocking_issue.blocked?
720 end
732 end
721
733
722 def test_blocked_issues_dont_allow_closed_statuses
734 def test_blocked_issues_dont_allow_closed_statuses
723 blocked_issue = Issue.find(9)
735 blocked_issue = Issue.find(9)
724
736
725 allowed_statuses = blocked_issue.new_statuses_allowed_to(users(:users_002))
737 allowed_statuses = blocked_issue.new_statuses_allowed_to(users(:users_002))
726 assert !allowed_statuses.empty?
738 assert !allowed_statuses.empty?
727 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
739 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
728 assert closed_statuses.empty?
740 assert closed_statuses.empty?
729 end
741 end
730
742
731 def test_unblocked_issues_allow_closed_statuses
743 def test_unblocked_issues_allow_closed_statuses
732 blocking_issue = Issue.find(10)
744 blocking_issue = Issue.find(10)
733
745
734 allowed_statuses = blocking_issue.new_statuses_allowed_to(users(:users_002))
746 allowed_statuses = blocking_issue.new_statuses_allowed_to(users(:users_002))
735 assert !allowed_statuses.empty?
747 assert !allowed_statuses.empty?
736 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
748 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
737 assert !closed_statuses.empty?
749 assert !closed_statuses.empty?
738 end
750 end
739
751
740 def test_rescheduling_an_issue_should_reschedule_following_issue
752 def test_rescheduling_an_issue_should_reschedule_following_issue
741 issue1 = Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => '-', :start_date => Date.today, :due_date => Date.today + 2)
753 issue1 = Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => '-', :start_date => Date.today, :due_date => Date.today + 2)
742 issue2 = Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => '-', :start_date => Date.today, :due_date => Date.today + 2)
754 issue2 = Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => '-', :start_date => Date.today, :due_date => Date.today + 2)
743 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_PRECEDES)
755 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_PRECEDES)
744 assert_equal issue1.due_date + 1, issue2.reload.start_date
756 assert_equal issue1.due_date + 1, issue2.reload.start_date
745
757
746 issue1.due_date = Date.today + 5
758 issue1.due_date = Date.today + 5
747 issue1.save!
759 issue1.save!
748 assert_equal issue1.due_date + 1, issue2.reload.start_date
760 assert_equal issue1.due_date + 1, issue2.reload.start_date
749 end
761 end
750
762
751 def test_overdue
763 def test_overdue
752 assert Issue.new(:due_date => 1.day.ago.to_date).overdue?
764 assert Issue.new(:due_date => 1.day.ago.to_date).overdue?
753 assert !Issue.new(:due_date => Date.today).overdue?
765 assert !Issue.new(:due_date => Date.today).overdue?
754 assert !Issue.new(:due_date => 1.day.from_now.to_date).overdue?
766 assert !Issue.new(:due_date => 1.day.from_now.to_date).overdue?
755 assert !Issue.new(:due_date => nil).overdue?
767 assert !Issue.new(:due_date => nil).overdue?
756 assert !Issue.new(:due_date => 1.day.ago.to_date, :status => IssueStatus.find(:first, :conditions => {:is_closed => true})).overdue?
768 assert !Issue.new(:due_date => 1.day.ago.to_date, :status => IssueStatus.find(:first, :conditions => {:is_closed => true})).overdue?
757 end
769 end
758
770
759 context "#behind_schedule?" do
771 context "#behind_schedule?" do
760 should "be false if the issue has no start_date" do
772 should "be false if the issue has no start_date" do
761 assert !Issue.new(:start_date => nil, :due_date => 1.day.from_now.to_date, :done_ratio => 0).behind_schedule?
773 assert !Issue.new(:start_date => nil, :due_date => 1.day.from_now.to_date, :done_ratio => 0).behind_schedule?
762 end
774 end
763
775
764 should "be false if the issue has no end_date" do
776 should "be false if the issue has no end_date" do
765 assert !Issue.new(:start_date => 1.day.from_now.to_date, :due_date => nil, :done_ratio => 0).behind_schedule?
777 assert !Issue.new(:start_date => 1.day.from_now.to_date, :due_date => nil, :done_ratio => 0).behind_schedule?
766 end
778 end
767
779
768 should "be false if the issue has more done than it's calendar time" do
780 should "be false if the issue has more done than it's calendar time" do
769 assert !Issue.new(:start_date => 50.days.ago.to_date, :due_date => 50.days.from_now.to_date, :done_ratio => 90).behind_schedule?
781 assert !Issue.new(:start_date => 50.days.ago.to_date, :due_date => 50.days.from_now.to_date, :done_ratio => 90).behind_schedule?
770 end
782 end
771
783
772 should "be true if the issue hasn't been started at all" do
784 should "be true if the issue hasn't been started at all" do
773 assert Issue.new(:start_date => 1.day.ago.to_date, :due_date => 1.day.from_now.to_date, :done_ratio => 0).behind_schedule?
785 assert Issue.new(:start_date => 1.day.ago.to_date, :due_date => 1.day.from_now.to_date, :done_ratio => 0).behind_schedule?
774 end
786 end
775
787
776 should "be true if the issue has used more calendar time than it's done ratio" do
788 should "be true if the issue has used more calendar time than it's done ratio" do
777 assert Issue.new(:start_date => 100.days.ago.to_date, :due_date => Date.today, :done_ratio => 90).behind_schedule?
789 assert Issue.new(:start_date => 100.days.ago.to_date, :due_date => Date.today, :done_ratio => 90).behind_schedule?
778 end
790 end
779 end
791 end
780
792
781 context "#assignable_users" do
793 context "#assignable_users" do
782 should "be Users" do
794 should "be Users" do
783 assert_kind_of User, Issue.find(1).assignable_users.first
795 assert_kind_of User, Issue.find(1).assignable_users.first
784 end
796 end
785
797
786 should "include the issue author" do
798 should "include the issue author" do
787 project = Project.find(1)
799 project = Project.find(1)
788 non_project_member = User.generate!
800 non_project_member = User.generate!
789 issue = Issue.generate_for_project!(project, :author => non_project_member)
801 issue = Issue.generate_for_project!(project, :author => non_project_member)
790
802
791 assert issue.assignable_users.include?(non_project_member)
803 assert issue.assignable_users.include?(non_project_member)
792 end
804 end
793
805
794 should "include the current assignee" do
806 should "include the current assignee" do
795 project = Project.find(1)
807 project = Project.find(1)
796 user = User.generate!
808 user = User.generate!
797 issue = Issue.generate_for_project!(project, :assigned_to => user)
809 issue = Issue.generate_for_project!(project, :assigned_to => user)
798 user.lock!
810 user.lock!
799
811
800 assert Issue.find(issue.id).assignable_users.include?(user)
812 assert Issue.find(issue.id).assignable_users.include?(user)
801 end
813 end
802
814
803 should "not show the issue author twice" do
815 should "not show the issue author twice" do
804 assignable_user_ids = Issue.find(1).assignable_users.collect(&:id)
816 assignable_user_ids = Issue.find(1).assignable_users.collect(&:id)
805 assert_equal 2, assignable_user_ids.length
817 assert_equal 2, assignable_user_ids.length
806
818
807 assignable_user_ids.each do |user_id|
819 assignable_user_ids.each do |user_id|
808 assert_equal 1, assignable_user_ids.select {|i| i == user_id}.length, "User #{user_id} appears more or less than once"
820 assert_equal 1, assignable_user_ids.select {|i| i == user_id}.length, "User #{user_id} appears more or less than once"
809 end
821 end
810 end
822 end
811
823
812 context "with issue_group_assignment" do
824 context "with issue_group_assignment" do
813 should "include groups" do
825 should "include groups" do
814 issue = Issue.new(:project => Project.find(2))
826 issue = Issue.new(:project => Project.find(2))
815
827
816 with_settings :issue_group_assignment => '1' do
828 with_settings :issue_group_assignment => '1' do
817 assert_equal %w(Group User), issue.assignable_users.map {|a| a.class.name}.uniq.sort
829 assert_equal %w(Group User), issue.assignable_users.map {|a| a.class.name}.uniq.sort
818 assert issue.assignable_users.include?(Group.find(11))
830 assert issue.assignable_users.include?(Group.find(11))
819 end
831 end
820 end
832 end
821 end
833 end
822
834
823 context "without issue_group_assignment" do
835 context "without issue_group_assignment" do
824 should "not include groups" do
836 should "not include groups" do
825 issue = Issue.new(:project => Project.find(2))
837 issue = Issue.new(:project => Project.find(2))
826
838
827 with_settings :issue_group_assignment => '0' do
839 with_settings :issue_group_assignment => '0' do
828 assert_equal %w(User), issue.assignable_users.map {|a| a.class.name}.uniq.sort
840 assert_equal %w(User), issue.assignable_users.map {|a| a.class.name}.uniq.sort
829 assert !issue.assignable_users.include?(Group.find(11))
841 assert !issue.assignable_users.include?(Group.find(11))
830 end
842 end
831 end
843 end
832 end
844 end
833 end
845 end
834
846
835 def test_create_should_send_email_notification
847 def test_create_should_send_email_notification
836 ActionMailer::Base.deliveries.clear
848 ActionMailer::Base.deliveries.clear
837 issue = Issue.new(:project_id => 1, :tracker_id => 1,
849 issue = Issue.new(:project_id => 1, :tracker_id => 1,
838 :author_id => 3, :status_id => 1,
850 :author_id => 3, :status_id => 1,
839 :priority => IssuePriority.all.first,
851 :priority => IssuePriority.all.first,
840 :subject => 'test_create', :estimated_hours => '1:30')
852 :subject => 'test_create', :estimated_hours => '1:30')
841
853
842 assert issue.save
854 assert issue.save
843 assert_equal 1, ActionMailer::Base.deliveries.size
855 assert_equal 1, ActionMailer::Base.deliveries.size
844 end
856 end
845
857
846 def test_stale_issue_should_not_send_email_notification
858 def test_stale_issue_should_not_send_email_notification
847 ActionMailer::Base.deliveries.clear
859 ActionMailer::Base.deliveries.clear
848 issue = Issue.find(1)
860 issue = Issue.find(1)
849 stale = Issue.find(1)
861 stale = Issue.find(1)
850
862
851 issue.init_journal(User.find(1))
863 issue.init_journal(User.find(1))
852 issue.subject = 'Subjet update'
864 issue.subject = 'Subjet update'
853 assert issue.save
865 assert issue.save
854 assert_equal 1, ActionMailer::Base.deliveries.size
866 assert_equal 1, ActionMailer::Base.deliveries.size
855 ActionMailer::Base.deliveries.clear
867 ActionMailer::Base.deliveries.clear
856
868
857 stale.init_journal(User.find(1))
869 stale.init_journal(User.find(1))
858 stale.subject = 'Another subjet update'
870 stale.subject = 'Another subjet update'
859 assert_raise ActiveRecord::StaleObjectError do
871 assert_raise ActiveRecord::StaleObjectError do
860 stale.save
872 stale.save
861 end
873 end
862 assert ActionMailer::Base.deliveries.empty?
874 assert ActionMailer::Base.deliveries.empty?
863 end
875 end
864
876
865 def test_journalized_description
877 def test_journalized_description
866 IssueCustomField.delete_all
878 IssueCustomField.delete_all
867
879
868 i = Issue.first
880 i = Issue.first
869 old_description = i.description
881 old_description = i.description
870 new_description = "This is the new description"
882 new_description = "This is the new description"
871
883
872 i.init_journal(User.find(2))
884 i.init_journal(User.find(2))
873 i.description = new_description
885 i.description = new_description
874 assert_difference 'Journal.count', 1 do
886 assert_difference 'Journal.count', 1 do
875 assert_difference 'JournalDetail.count', 1 do
887 assert_difference 'JournalDetail.count', 1 do
876 i.save!
888 i.save!
877 end
889 end
878 end
890 end
879
891
880 detail = JournalDetail.first(:order => 'id DESC')
892 detail = JournalDetail.first(:order => 'id DESC')
881 assert_equal i, detail.journal.journalized
893 assert_equal i, detail.journal.journalized
882 assert_equal 'attr', detail.property
894 assert_equal 'attr', detail.property
883 assert_equal 'description', detail.prop_key
895 assert_equal 'description', detail.prop_key
884 assert_equal old_description, detail.old_value
896 assert_equal old_description, detail.old_value
885 assert_equal new_description, detail.value
897 assert_equal new_description, detail.value
886 end
898 end
887
899
888 def test_blank_descriptions_should_not_be_journalized
900 def test_blank_descriptions_should_not_be_journalized
889 IssueCustomField.delete_all
901 IssueCustomField.delete_all
890 Issue.update_all("description = NULL", "id=1")
902 Issue.update_all("description = NULL", "id=1")
891
903
892 i = Issue.find(1)
904 i = Issue.find(1)
893 i.init_journal(User.find(2))
905 i.init_journal(User.find(2))
894 i.subject = "blank description"
906 i.subject = "blank description"
895 i.description = "\r\n"
907 i.description = "\r\n"
896
908
897 assert_difference 'Journal.count', 1 do
909 assert_difference 'Journal.count', 1 do
898 assert_difference 'JournalDetail.count', 1 do
910 assert_difference 'JournalDetail.count', 1 do
899 i.save!
911 i.save!
900 end
912 end
901 end
913 end
902 end
914 end
903
915
904 def test_description_eol_should_be_normalized
916 def test_description_eol_should_be_normalized
905 i = Issue.new(:description => "CR \r LF \n CRLF \r\n")
917 i = Issue.new(:description => "CR \r LF \n CRLF \r\n")
906 assert_equal "CR \r\n LF \r\n CRLF \r\n", i.description
918 assert_equal "CR \r\n LF \r\n CRLF \r\n", i.description
907 end
919 end
908
920
909 def test_saving_twice_should_not_duplicate_journal_details
921 def test_saving_twice_should_not_duplicate_journal_details
910 i = Issue.find(:first)
922 i = Issue.find(:first)
911 i.init_journal(User.find(2), 'Some notes')
923 i.init_journal(User.find(2), 'Some notes')
912 # initial changes
924 # initial changes
913 i.subject = 'New subject'
925 i.subject = 'New subject'
914 i.done_ratio = i.done_ratio + 10
926 i.done_ratio = i.done_ratio + 10
915 assert_difference 'Journal.count' do
927 assert_difference 'Journal.count' do
916 assert i.save
928 assert i.save
917 end
929 end
918 # 1 more change
930 # 1 more change
919 i.priority = IssuePriority.find(:first, :conditions => ["id <> ?", i.priority_id])
931 i.priority = IssuePriority.find(:first, :conditions => ["id <> ?", i.priority_id])
920 assert_no_difference 'Journal.count' do
932 assert_no_difference 'Journal.count' do
921 assert_difference 'JournalDetail.count', 1 do
933 assert_difference 'JournalDetail.count', 1 do
922 i.save
934 i.save
923 end
935 end
924 end
936 end
925 # no more change
937 # no more change
926 assert_no_difference 'Journal.count' do
938 assert_no_difference 'Journal.count' do
927 assert_no_difference 'JournalDetail.count' do
939 assert_no_difference 'JournalDetail.count' do
928 i.save
940 i.save
929 end
941 end
930 end
942 end
931 end
943 end
932
944
933 def test_all_dependent_issues
945 def test_all_dependent_issues
934 IssueRelation.delete_all
946 IssueRelation.delete_all
935 assert IssueRelation.create!(:issue_from => Issue.find(1),
947 assert IssueRelation.create!(:issue_from => Issue.find(1),
936 :issue_to => Issue.find(2),
948 :issue_to => Issue.find(2),
937 :relation_type => IssueRelation::TYPE_PRECEDES)
949 :relation_type => IssueRelation::TYPE_PRECEDES)
938 assert IssueRelation.create!(:issue_from => Issue.find(2),
950 assert IssueRelation.create!(:issue_from => Issue.find(2),
939 :issue_to => Issue.find(3),
951 :issue_to => Issue.find(3),
940 :relation_type => IssueRelation::TYPE_PRECEDES)
952 :relation_type => IssueRelation::TYPE_PRECEDES)
941 assert IssueRelation.create!(:issue_from => Issue.find(3),
953 assert IssueRelation.create!(:issue_from => Issue.find(3),
942 :issue_to => Issue.find(8),
954 :issue_to => Issue.find(8),
943 :relation_type => IssueRelation::TYPE_PRECEDES)
955 :relation_type => IssueRelation::TYPE_PRECEDES)
944
956
945 assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort
957 assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort
946 end
958 end
947
959
948 def test_all_dependent_issues_with_persistent_circular_dependency
960 def test_all_dependent_issues_with_persistent_circular_dependency
949 IssueRelation.delete_all
961 IssueRelation.delete_all
950 assert IssueRelation.create!(:issue_from => Issue.find(1),
962 assert IssueRelation.create!(:issue_from => Issue.find(1),
951 :issue_to => Issue.find(2),
963 :issue_to => Issue.find(2),
952 :relation_type => IssueRelation::TYPE_PRECEDES)
964 :relation_type => IssueRelation::TYPE_PRECEDES)
953 assert IssueRelation.create!(:issue_from => Issue.find(2),
965 assert IssueRelation.create!(:issue_from => Issue.find(2),
954 :issue_to => Issue.find(3),
966 :issue_to => Issue.find(3),
955 :relation_type => IssueRelation::TYPE_PRECEDES)
967 :relation_type => IssueRelation::TYPE_PRECEDES)
956 # Validation skipping
968 # Validation skipping
957 assert IssueRelation.new(:issue_from => Issue.find(3),
969 assert IssueRelation.new(:issue_from => Issue.find(3),
958 :issue_to => Issue.find(1),
970 :issue_to => Issue.find(1),
959 :relation_type => IssueRelation::TYPE_PRECEDES).save(false)
971 :relation_type => IssueRelation::TYPE_PRECEDES).save(false)
960
972
961 assert_equal [2, 3], Issue.find(1).all_dependent_issues.collect(&:id).sort
973 assert_equal [2, 3], Issue.find(1).all_dependent_issues.collect(&:id).sort
962 end
974 end
963
975
964 def test_all_dependent_issues_with_persistent_multiple_circular_dependencies
976 def test_all_dependent_issues_with_persistent_multiple_circular_dependencies
965 IssueRelation.delete_all
977 IssueRelation.delete_all
966 assert IssueRelation.create!(:issue_from => Issue.find(1),
978 assert IssueRelation.create!(:issue_from => Issue.find(1),
967 :issue_to => Issue.find(2),
979 :issue_to => Issue.find(2),
968 :relation_type => IssueRelation::TYPE_RELATES)
980 :relation_type => IssueRelation::TYPE_RELATES)
969 assert IssueRelation.create!(:issue_from => Issue.find(2),
981 assert IssueRelation.create!(:issue_from => Issue.find(2),
970 :issue_to => Issue.find(3),
982 :issue_to => Issue.find(3),
971 :relation_type => IssueRelation::TYPE_RELATES)
983 :relation_type => IssueRelation::TYPE_RELATES)
972 assert IssueRelation.create!(:issue_from => Issue.find(3),
984 assert IssueRelation.create!(:issue_from => Issue.find(3),
973 :issue_to => Issue.find(8),
985 :issue_to => Issue.find(8),
974 :relation_type => IssueRelation::TYPE_RELATES)
986 :relation_type => IssueRelation::TYPE_RELATES)
975 # Validation skipping
987 # Validation skipping
976 assert IssueRelation.new(:issue_from => Issue.find(8),
988 assert IssueRelation.new(:issue_from => Issue.find(8),
977 :issue_to => Issue.find(2),
989 :issue_to => Issue.find(2),
978 :relation_type => IssueRelation::TYPE_RELATES).save(false)
990 :relation_type => IssueRelation::TYPE_RELATES).save(false)
979 assert IssueRelation.new(:issue_from => Issue.find(3),
991 assert IssueRelation.new(:issue_from => Issue.find(3),
980 :issue_to => Issue.find(1),
992 :issue_to => Issue.find(1),
981 :relation_type => IssueRelation::TYPE_RELATES).save(false)
993 :relation_type => IssueRelation::TYPE_RELATES).save(false)
982
994
983 assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort
995 assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort
984 end
996 end
985
997
986 context "#done_ratio" do
998 context "#done_ratio" do
987 setup do
999 setup do
988 @issue = Issue.find(1)
1000 @issue = Issue.find(1)
989 @issue_status = IssueStatus.find(1)
1001 @issue_status = IssueStatus.find(1)
990 @issue_status.update_attribute(:default_done_ratio, 50)
1002 @issue_status.update_attribute(:default_done_ratio, 50)
991 @issue2 = Issue.find(2)
1003 @issue2 = Issue.find(2)
992 @issue_status2 = IssueStatus.find(2)
1004 @issue_status2 = IssueStatus.find(2)
993 @issue_status2.update_attribute(:default_done_ratio, 0)
1005 @issue_status2.update_attribute(:default_done_ratio, 0)
994 end
1006 end
995
1007
996 teardown do
1008 teardown do
997 Setting.issue_done_ratio = 'issue_field'
1009 Setting.issue_done_ratio = 'issue_field'
998 end
1010 end
999
1011
1000 context "with Setting.issue_done_ratio using the issue_field" do
1012 context "with Setting.issue_done_ratio using the issue_field" do
1001 setup do
1013 setup do
1002 Setting.issue_done_ratio = 'issue_field'
1014 Setting.issue_done_ratio = 'issue_field'
1003 end
1015 end
1004
1016
1005 should "read the issue's field" do
1017 should "read the issue's field" do
1006 assert_equal 0, @issue.done_ratio
1018 assert_equal 0, @issue.done_ratio
1007 assert_equal 30, @issue2.done_ratio
1019 assert_equal 30, @issue2.done_ratio
1008 end
1020 end
1009 end
1021 end
1010
1022
1011 context "with Setting.issue_done_ratio using the issue_status" do
1023 context "with Setting.issue_done_ratio using the issue_status" do
1012 setup do
1024 setup do
1013 Setting.issue_done_ratio = 'issue_status'
1025 Setting.issue_done_ratio = 'issue_status'
1014 end
1026 end
1015
1027
1016 should "read the Issue Status's default done ratio" do
1028 should "read the Issue Status's default done ratio" do
1017 assert_equal 50, @issue.done_ratio
1029 assert_equal 50, @issue.done_ratio
1018 assert_equal 0, @issue2.done_ratio
1030 assert_equal 0, @issue2.done_ratio
1019 end
1031 end
1020 end
1032 end
1021 end
1033 end
1022
1034
1023 context "#update_done_ratio_from_issue_status" do
1035 context "#update_done_ratio_from_issue_status" do
1024 setup do
1036 setup do
1025 @issue = Issue.find(1)
1037 @issue = Issue.find(1)
1026 @issue_status = IssueStatus.find(1)
1038 @issue_status = IssueStatus.find(1)
1027 @issue_status.update_attribute(:default_done_ratio, 50)
1039 @issue_status.update_attribute(:default_done_ratio, 50)
1028 @issue2 = Issue.find(2)
1040 @issue2 = Issue.find(2)
1029 @issue_status2 = IssueStatus.find(2)
1041 @issue_status2 = IssueStatus.find(2)
1030 @issue_status2.update_attribute(:default_done_ratio, 0)
1042 @issue_status2.update_attribute(:default_done_ratio, 0)
1031 end
1043 end
1032
1044
1033 context "with Setting.issue_done_ratio using the issue_field" do
1045 context "with Setting.issue_done_ratio using the issue_field" do
1034 setup do
1046 setup do
1035 Setting.issue_done_ratio = 'issue_field'
1047 Setting.issue_done_ratio = 'issue_field'
1036 end
1048 end
1037
1049
1038 should "not change the issue" do
1050 should "not change the issue" do
1039 @issue.update_done_ratio_from_issue_status
1051 @issue.update_done_ratio_from_issue_status
1040 @issue2.update_done_ratio_from_issue_status
1052 @issue2.update_done_ratio_from_issue_status
1041
1053
1042 assert_equal 0, @issue.read_attribute(:done_ratio)
1054 assert_equal 0, @issue.read_attribute(:done_ratio)
1043 assert_equal 30, @issue2.read_attribute(:done_ratio)
1055 assert_equal 30, @issue2.read_attribute(:done_ratio)
1044 end
1056 end
1045 end
1057 end
1046
1058
1047 context "with Setting.issue_done_ratio using the issue_status" do
1059 context "with Setting.issue_done_ratio using the issue_status" do
1048 setup do
1060 setup do
1049 Setting.issue_done_ratio = 'issue_status'
1061 Setting.issue_done_ratio = 'issue_status'
1050 end
1062 end
1051
1063
1052 should "change the issue's done ratio" do
1064 should "change the issue's done ratio" do
1053 @issue.update_done_ratio_from_issue_status
1065 @issue.update_done_ratio_from_issue_status
1054 @issue2.update_done_ratio_from_issue_status
1066 @issue2.update_done_ratio_from_issue_status
1055
1067
1056 assert_equal 50, @issue.read_attribute(:done_ratio)
1068 assert_equal 50, @issue.read_attribute(:done_ratio)
1057 assert_equal 0, @issue2.read_attribute(:done_ratio)
1069 assert_equal 0, @issue2.read_attribute(:done_ratio)
1058 end
1070 end
1059 end
1071 end
1060 end
1072 end
1061
1073
1062 test "#by_tracker" do
1074 test "#by_tracker" do
1063 User.current = User.anonymous
1075 User.current = User.anonymous
1064 groups = Issue.by_tracker(Project.find(1))
1076 groups = Issue.by_tracker(Project.find(1))
1065 assert_equal 3, groups.size
1077 assert_equal 3, groups.size
1066 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1078 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1067 end
1079 end
1068
1080
1069 test "#by_version" do
1081 test "#by_version" do
1070 User.current = User.anonymous
1082 User.current = User.anonymous
1071 groups = Issue.by_version(Project.find(1))
1083 groups = Issue.by_version(Project.find(1))
1072 assert_equal 3, groups.size
1084 assert_equal 3, groups.size
1073 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1085 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1074 end
1086 end
1075
1087
1076 test "#by_priority" do
1088 test "#by_priority" do
1077 User.current = User.anonymous
1089 User.current = User.anonymous
1078 groups = Issue.by_priority(Project.find(1))
1090 groups = Issue.by_priority(Project.find(1))
1079 assert_equal 4, groups.size
1091 assert_equal 4, groups.size
1080 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1092 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1081 end
1093 end
1082
1094
1083 test "#by_category" do
1095 test "#by_category" do
1084 User.current = User.anonymous
1096 User.current = User.anonymous
1085 groups = Issue.by_category(Project.find(1))
1097 groups = Issue.by_category(Project.find(1))
1086 assert_equal 2, groups.size
1098 assert_equal 2, groups.size
1087 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1099 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1088 end
1100 end
1089
1101
1090 test "#by_assigned_to" do
1102 test "#by_assigned_to" do
1091 User.current = User.anonymous
1103 User.current = User.anonymous
1092 groups = Issue.by_assigned_to(Project.find(1))
1104 groups = Issue.by_assigned_to(Project.find(1))
1093 assert_equal 2, groups.size
1105 assert_equal 2, groups.size
1094 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1106 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1095 end
1107 end
1096
1108
1097 test "#by_author" do
1109 test "#by_author" do
1098 User.current = User.anonymous
1110 User.current = User.anonymous
1099 groups = Issue.by_author(Project.find(1))
1111 groups = Issue.by_author(Project.find(1))
1100 assert_equal 4, groups.size
1112 assert_equal 4, groups.size
1101 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1113 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1102 end
1114 end
1103
1115
1104 test "#by_subproject" do
1116 test "#by_subproject" do
1105 User.current = User.anonymous
1117 User.current = User.anonymous
1106 groups = Issue.by_subproject(Project.find(1))
1118 groups = Issue.by_subproject(Project.find(1))
1107 # Private descendant not visible
1119 # Private descendant not visible
1108 assert_equal 1, groups.size
1120 assert_equal 1, groups.size
1109 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1121 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1110 end
1122 end
1111
1123
1112 context ".allowed_target_projects_on_move" do
1124 context ".allowed_target_projects_on_move" do
1113 should "return all active projects for admin users" do
1125 should "return all active projects for admin users" do
1114 User.current = User.find(1)
1126 User.current = User.find(1)
1115 assert_equal Project.active.count, Issue.allowed_target_projects_on_move.size
1127 assert_equal Project.active.count, Issue.allowed_target_projects_on_move.size
1116 end
1128 end
1117
1129
1118 should "return allowed projects for non admin users" do
1130 should "return allowed projects for non admin users" do
1119 User.current = User.find(2)
1131 User.current = User.find(2)
1120 Role.non_member.remove_permission! :move_issues
1132 Role.non_member.remove_permission! :move_issues
1121 assert_equal 3, Issue.allowed_target_projects_on_move.size
1133 assert_equal 3, Issue.allowed_target_projects_on_move.size
1122
1134
1123 Role.non_member.add_permission! :move_issues
1135 Role.non_member.add_permission! :move_issues
1124 assert_equal Project.active.count, Issue.allowed_target_projects_on_move.size
1136 assert_equal Project.active.count, Issue.allowed_target_projects_on_move.size
1125 end
1137 end
1126 end
1138 end
1127
1139
1128 def test_recently_updated_with_limit_scopes
1140 def test_recently_updated_with_limit_scopes
1129 #should return the last updated issue
1141 #should return the last updated issue
1130 assert_equal 1, Issue.recently_updated.with_limit(1).length
1142 assert_equal 1, Issue.recently_updated.with_limit(1).length
1131 assert_equal Issue.find(:first, :order => "updated_on DESC"), Issue.recently_updated.with_limit(1).first
1143 assert_equal Issue.find(:first, :order => "updated_on DESC"), Issue.recently_updated.with_limit(1).first
1132 end
1144 end
1133
1145
1134 def test_on_active_projects_scope
1146 def test_on_active_projects_scope
1135 assert Project.find(2).archive
1147 assert Project.find(2).archive
1136
1148
1137 before = Issue.on_active_project.length
1149 before = Issue.on_active_project.length
1138 # test inclusion to results
1150 # test inclusion to results
1139 issue = Issue.generate_for_project!(Project.find(1), :tracker => Project.find(2).trackers.first)
1151 issue = Issue.generate_for_project!(Project.find(1), :tracker => Project.find(2).trackers.first)
1140 assert_equal before + 1, Issue.on_active_project.length
1152 assert_equal before + 1, Issue.on_active_project.length
1141
1153
1142 # Move to an archived project
1154 # Move to an archived project
1143 issue.project = Project.find(2)
1155 issue.project = Project.find(2)
1144 assert issue.save
1156 assert issue.save
1145 assert_equal before, Issue.on_active_project.length
1157 assert_equal before, Issue.on_active_project.length
1146 end
1158 end
1147
1159
1148 context "Issue#recipients" do
1160 context "Issue#recipients" do
1149 setup do
1161 setup do
1150 @project = Project.find(1)
1162 @project = Project.find(1)
1151 @author = User.generate_with_protected!
1163 @author = User.generate_with_protected!
1152 @assignee = User.generate_with_protected!
1164 @assignee = User.generate_with_protected!
1153 @issue = Issue.generate_for_project!(@project, :assigned_to => @assignee, :author => @author)
1165 @issue = Issue.generate_for_project!(@project, :assigned_to => @assignee, :author => @author)
1154 end
1166 end
1155
1167
1156 should "include project recipients" do
1168 should "include project recipients" do
1157 assert @project.recipients.present?
1169 assert @project.recipients.present?
1158 @project.recipients.each do |project_recipient|
1170 @project.recipients.each do |project_recipient|
1159 assert @issue.recipients.include?(project_recipient)
1171 assert @issue.recipients.include?(project_recipient)
1160 end
1172 end
1161 end
1173 end
1162
1174
1163 should "include the author if the author is active" do
1175 should "include the author if the author is active" do
1164 assert @issue.author, "No author set for Issue"
1176 assert @issue.author, "No author set for Issue"
1165 assert @issue.recipients.include?(@issue.author.mail)
1177 assert @issue.recipients.include?(@issue.author.mail)
1166 end
1178 end
1167
1179
1168 should "include the assigned to user if the assigned to user is active" do
1180 should "include the assigned to user if the assigned to user is active" do
1169 assert @issue.assigned_to, "No assigned_to set for Issue"
1181 assert @issue.assigned_to, "No assigned_to set for Issue"
1170 assert @issue.recipients.include?(@issue.assigned_to.mail)
1182 assert @issue.recipients.include?(@issue.assigned_to.mail)
1171 end
1183 end
1172
1184
1173 should "not include users who opt out of all email" do
1185 should "not include users who opt out of all email" do
1174 @author.update_attribute(:mail_notification, :none)
1186 @author.update_attribute(:mail_notification, :none)
1175
1187
1176 assert !@issue.recipients.include?(@issue.author.mail)
1188 assert !@issue.recipients.include?(@issue.author.mail)
1177 end
1189 end
1178
1190
1179 should "not include the issue author if they are only notified of assigned issues" do
1191 should "not include the issue author if they are only notified of assigned issues" do
1180 @author.update_attribute(:mail_notification, :only_assigned)
1192 @author.update_attribute(:mail_notification, :only_assigned)
1181
1193
1182 assert !@issue.recipients.include?(@issue.author.mail)
1194 assert !@issue.recipients.include?(@issue.author.mail)
1183 end
1195 end
1184
1196
1185 should "not include the assigned user if they are only notified of owned issues" do
1197 should "not include the assigned user if they are only notified of owned issues" do
1186 @assignee.update_attribute(:mail_notification, :only_owner)
1198 @assignee.update_attribute(:mail_notification, :only_owner)
1187
1199
1188 assert !@issue.recipients.include?(@issue.assigned_to.mail)
1200 assert !@issue.recipients.include?(@issue.assigned_to.mail)
1189 end
1201 end
1190
1202
1191 end
1203 end
1192 end
1204 end
General Comments 0
You need to be logged in to leave comments. Login now