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