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