##// END OF EJS Templates
Fixed: submitting a non numerical parent task input creates a 500 error (#6932)....
Jean-Philippe Lang -
r4300:06b0176a3edd
parent child
Show More
@@ -1,887 +1,887
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 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 belongs_to :project
19 belongs_to :project
20 belongs_to :tracker
20 belongs_to :tracker
21 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
21 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
22 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
22 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
23 belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id'
23 belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id'
24 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
24 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
25 belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
25 belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
26 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
26 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
27
27
28 has_many :journals, :as => :journalized, :dependent => :destroy
28 has_many :journals, :as => :journalized, :dependent => :destroy
29 has_many :time_entries, :dependent => :delete_all
29 has_many :time_entries, :dependent => :delete_all
30 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
30 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
31
31
32 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
32 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
33 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
33 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
34
34
35 acts_as_nested_set :scope => 'root_id'
35 acts_as_nested_set :scope => 'root_id'
36 acts_as_attachable :after_remove => :attachment_removed
36 acts_as_attachable :after_remove => :attachment_removed
37 acts_as_customizable
37 acts_as_customizable
38 acts_as_watchable
38 acts_as_watchable
39 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
39 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
40 :include => [:project, :journals],
40 :include => [:project, :journals],
41 # sort by id so that limited eager loading doesn't break with postgresql
41 # sort by id so that limited eager loading doesn't break with postgresql
42 :order_column => "#{table_name}.id"
42 :order_column => "#{table_name}.id"
43 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
43 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
44 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
44 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
45 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
45 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
46
46
47 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
47 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
48 :author_key => :author_id
48 :author_key => :author_id
49
49
50 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
50 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
51
51
52 attr_reader :current_journal
52 attr_reader :current_journal
53
53
54 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
54 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
55
55
56 validates_length_of :subject, :maximum => 255
56 validates_length_of :subject, :maximum => 255
57 validates_inclusion_of :done_ratio, :in => 0..100
57 validates_inclusion_of :done_ratio, :in => 0..100
58 validates_numericality_of :estimated_hours, :allow_nil => true
58 validates_numericality_of :estimated_hours, :allow_nil => true
59
59
60 named_scope :visible, lambda {|*args| { :include => :project,
60 named_scope :visible, lambda {|*args| { :include => :project,
61 :conditions => Project.allowed_to_condition(args.first || User.current, :view_issues) } }
61 :conditions => Project.allowed_to_condition(args.first || User.current, :view_issues) } }
62
62
63 named_scope :open, :conditions => ["#{IssueStatus.table_name}.is_closed = ?", false], :include => :status
63 named_scope :open, :conditions => ["#{IssueStatus.table_name}.is_closed = ?", false], :include => :status
64
64
65 named_scope :recently_updated, :order => "#{Issue.table_name}.updated_on DESC"
65 named_scope :recently_updated, :order => "#{Issue.table_name}.updated_on DESC"
66 named_scope :with_limit, lambda { |limit| { :limit => limit} }
66 named_scope :with_limit, lambda { |limit| { :limit => limit} }
67 named_scope :on_active_project, :include => [:status, :project, :tracker],
67 named_scope :on_active_project, :include => [:status, :project, :tracker],
68 :conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"]
68 :conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"]
69 named_scope :for_gantt, lambda {
69 named_scope :for_gantt, lambda {
70 {
70 {
71 :include => [:tracker, :status, :assigned_to, :priority, :project, :fixed_version],
71 :include => [:tracker, :status, :assigned_to, :priority, :project, :fixed_version],
72 :order => "#{Issue.table_name}.due_date ASC, #{Issue.table_name}.start_date ASC, #{Issue.table_name}.id ASC"
72 :order => "#{Issue.table_name}.due_date ASC, #{Issue.table_name}.start_date ASC, #{Issue.table_name}.id ASC"
73 }
73 }
74 }
74 }
75
75
76 named_scope :without_version, lambda {
76 named_scope :without_version, lambda {
77 {
77 {
78 :conditions => { :fixed_version_id => nil}
78 :conditions => { :fixed_version_id => nil}
79 }
79 }
80 }
80 }
81
81
82 named_scope :with_query, lambda {|query|
82 named_scope :with_query, lambda {|query|
83 {
83 {
84 :conditions => Query.merge_conditions(query.statement)
84 :conditions => Query.merge_conditions(query.statement)
85 }
85 }
86 }
86 }
87
87
88 before_create :default_assign
88 before_create :default_assign
89 before_save :close_duplicates, :update_done_ratio_from_issue_status
89 before_save :close_duplicates, :update_done_ratio_from_issue_status
90 after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal
90 after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal
91 after_destroy :destroy_children
91 after_destroy :destroy_children
92 after_destroy :update_parent_attributes
92 after_destroy :update_parent_attributes
93
93
94 # Returns true if usr or current user is allowed to view the issue
94 # Returns true if usr or current user is allowed to view the issue
95 def visible?(usr=nil)
95 def visible?(usr=nil)
96 (usr || User.current).allowed_to?(:view_issues, self.project)
96 (usr || User.current).allowed_to?(:view_issues, self.project)
97 end
97 end
98
98
99 def after_initialize
99 def after_initialize
100 if new_record?
100 if new_record?
101 # set default values for new records only
101 # set default values for new records only
102 self.status ||= IssueStatus.default
102 self.status ||= IssueStatus.default
103 self.priority ||= IssuePriority.default
103 self.priority ||= IssuePriority.default
104 end
104 end
105 end
105 end
106
106
107 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
107 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
108 def available_custom_fields
108 def available_custom_fields
109 (project && tracker) ? project.all_issue_custom_fields.select {|c| tracker.custom_fields.include? c } : []
109 (project && tracker) ? project.all_issue_custom_fields.select {|c| tracker.custom_fields.include? c } : []
110 end
110 end
111
111
112 def copy_from(arg)
112 def copy_from(arg)
113 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
113 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
114 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
114 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
115 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
115 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
116 self.status = issue.status
116 self.status = issue.status
117 self
117 self
118 end
118 end
119
119
120 # Moves/copies an issue to a new project and tracker
120 # Moves/copies an issue to a new project and tracker
121 # Returns the moved/copied issue on success, false on failure
121 # Returns the moved/copied issue on success, false on failure
122 def move_to_project(*args)
122 def move_to_project(*args)
123 ret = Issue.transaction do
123 ret = Issue.transaction do
124 move_to_project_without_transaction(*args) || raise(ActiveRecord::Rollback)
124 move_to_project_without_transaction(*args) || raise(ActiveRecord::Rollback)
125 end || false
125 end || false
126 end
126 end
127
127
128 def move_to_project_without_transaction(new_project, new_tracker = nil, options = {})
128 def move_to_project_without_transaction(new_project, new_tracker = nil, options = {})
129 options ||= {}
129 options ||= {}
130 issue = options[:copy] ? self.class.new.copy_from(self) : self
130 issue = options[:copy] ? self.class.new.copy_from(self) : self
131
131
132 if new_project && issue.project_id != new_project.id
132 if new_project && issue.project_id != new_project.id
133 # delete issue relations
133 # delete issue relations
134 unless Setting.cross_project_issue_relations?
134 unless Setting.cross_project_issue_relations?
135 issue.relations_from.clear
135 issue.relations_from.clear
136 issue.relations_to.clear
136 issue.relations_to.clear
137 end
137 end
138 # issue is moved to another project
138 # issue is moved to another project
139 # reassign to the category with same name if any
139 # reassign to the category with same name if any
140 new_category = issue.category.nil? ? nil : new_project.issue_categories.find_by_name(issue.category.name)
140 new_category = issue.category.nil? ? nil : new_project.issue_categories.find_by_name(issue.category.name)
141 issue.category = new_category
141 issue.category = new_category
142 # Keep the fixed_version if it's still valid in the new_project
142 # Keep the fixed_version if it's still valid in the new_project
143 unless new_project.shared_versions.include?(issue.fixed_version)
143 unless new_project.shared_versions.include?(issue.fixed_version)
144 issue.fixed_version = nil
144 issue.fixed_version = nil
145 end
145 end
146 issue.project = new_project
146 issue.project = new_project
147 if issue.parent && issue.parent.project_id != issue.project_id
147 if issue.parent && issue.parent.project_id != issue.project_id
148 issue.parent_issue_id = nil
148 issue.parent_issue_id = nil
149 end
149 end
150 end
150 end
151 if new_tracker
151 if new_tracker
152 issue.tracker = new_tracker
152 issue.tracker = new_tracker
153 issue.reset_custom_values!
153 issue.reset_custom_values!
154 end
154 end
155 if options[:copy]
155 if options[:copy]
156 issue.custom_field_values = self.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
156 issue.custom_field_values = self.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
157 issue.status = if options[:attributes] && options[:attributes][:status_id]
157 issue.status = if options[:attributes] && options[:attributes][:status_id]
158 IssueStatus.find_by_id(options[:attributes][:status_id])
158 IssueStatus.find_by_id(options[:attributes][:status_id])
159 else
159 else
160 self.status
160 self.status
161 end
161 end
162 end
162 end
163 # Allow bulk setting of attributes on the issue
163 # Allow bulk setting of attributes on the issue
164 if options[:attributes]
164 if options[:attributes]
165 issue.attributes = options[:attributes]
165 issue.attributes = options[:attributes]
166 end
166 end
167 if issue.save
167 if issue.save
168 unless options[:copy]
168 unless options[:copy]
169 # Manually update project_id on related time entries
169 # Manually update project_id on related time entries
170 TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
170 TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
171
171
172 issue.children.each do |child|
172 issue.children.each do |child|
173 unless child.move_to_project_without_transaction(new_project)
173 unless child.move_to_project_without_transaction(new_project)
174 # Move failed and transaction was rollback'd
174 # Move failed and transaction was rollback'd
175 return false
175 return false
176 end
176 end
177 end
177 end
178 end
178 end
179 else
179 else
180 return false
180 return false
181 end
181 end
182 issue
182 issue
183 end
183 end
184
184
185 def status_id=(sid)
185 def status_id=(sid)
186 self.status = nil
186 self.status = nil
187 write_attribute(:status_id, sid)
187 write_attribute(:status_id, sid)
188 end
188 end
189
189
190 def priority_id=(pid)
190 def priority_id=(pid)
191 self.priority = nil
191 self.priority = nil
192 write_attribute(:priority_id, pid)
192 write_attribute(:priority_id, pid)
193 end
193 end
194
194
195 def tracker_id=(tid)
195 def tracker_id=(tid)
196 self.tracker = nil
196 self.tracker = nil
197 result = write_attribute(:tracker_id, tid)
197 result = write_attribute(:tracker_id, tid)
198 @custom_field_values = nil
198 @custom_field_values = nil
199 result
199 result
200 end
200 end
201
201
202 # Overrides attributes= so that tracker_id gets assigned first
202 # Overrides attributes= so that tracker_id gets assigned first
203 def attributes_with_tracker_first=(new_attributes, *args)
203 def attributes_with_tracker_first=(new_attributes, *args)
204 return if new_attributes.nil?
204 return if new_attributes.nil?
205 new_tracker_id = new_attributes['tracker_id'] || new_attributes[:tracker_id]
205 new_tracker_id = new_attributes['tracker_id'] || new_attributes[:tracker_id]
206 if new_tracker_id
206 if new_tracker_id
207 self.tracker_id = new_tracker_id
207 self.tracker_id = new_tracker_id
208 end
208 end
209 send :attributes_without_tracker_first=, new_attributes, *args
209 send :attributes_without_tracker_first=, new_attributes, *args
210 end
210 end
211 # Do not redefine alias chain on reload (see #4838)
211 # Do not redefine alias chain on reload (see #4838)
212 alias_method_chain(:attributes=, :tracker_first) unless method_defined?(:attributes_without_tracker_first=)
212 alias_method_chain(:attributes=, :tracker_first) unless method_defined?(:attributes_without_tracker_first=)
213
213
214 def estimated_hours=(h)
214 def estimated_hours=(h)
215 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
215 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
216 end
216 end
217
217
218 SAFE_ATTRIBUTES = %w(
218 SAFE_ATTRIBUTES = %w(
219 tracker_id
219 tracker_id
220 status_id
220 status_id
221 parent_issue_id
221 parent_issue_id
222 category_id
222 category_id
223 assigned_to_id
223 assigned_to_id
224 priority_id
224 priority_id
225 fixed_version_id
225 fixed_version_id
226 subject
226 subject
227 description
227 description
228 start_date
228 start_date
229 due_date
229 due_date
230 done_ratio
230 done_ratio
231 estimated_hours
231 estimated_hours
232 custom_field_values
232 custom_field_values
233 lock_version
233 lock_version
234 ) unless const_defined?(:SAFE_ATTRIBUTES)
234 ) unless const_defined?(:SAFE_ATTRIBUTES)
235
235
236 SAFE_ATTRIBUTES_ON_TRANSITION = %w(
236 SAFE_ATTRIBUTES_ON_TRANSITION = %w(
237 status_id
237 status_id
238 assigned_to_id
238 assigned_to_id
239 fixed_version_id
239 fixed_version_id
240 done_ratio
240 done_ratio
241 ) unless const_defined?(:SAFE_ATTRIBUTES_ON_TRANSITION)
241 ) unless const_defined?(:SAFE_ATTRIBUTES_ON_TRANSITION)
242
242
243 # Safely sets attributes
243 # Safely sets attributes
244 # Should be called from controllers instead of #attributes=
244 # Should be called from controllers instead of #attributes=
245 # attr_accessible is too rough because we still want things like
245 # attr_accessible is too rough because we still want things like
246 # Issue.new(:project => foo) to work
246 # Issue.new(:project => foo) to work
247 # TODO: move workflow/permission checks from controllers to here
247 # TODO: move workflow/permission checks from controllers to here
248 def safe_attributes=(attrs, user=User.current)
248 def safe_attributes=(attrs, user=User.current)
249 return unless attrs.is_a?(Hash)
249 return unless attrs.is_a?(Hash)
250
250
251 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
251 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
252 if new_record? || user.allowed_to?(:edit_issues, project)
252 if new_record? || user.allowed_to?(:edit_issues, project)
253 attrs = attrs.reject {|k,v| !SAFE_ATTRIBUTES.include?(k)}
253 attrs = attrs.reject {|k,v| !SAFE_ATTRIBUTES.include?(k)}
254 elsif new_statuses_allowed_to(user).any?
254 elsif new_statuses_allowed_to(user).any?
255 attrs = attrs.reject {|k,v| !SAFE_ATTRIBUTES_ON_TRANSITION.include?(k)}
255 attrs = attrs.reject {|k,v| !SAFE_ATTRIBUTES_ON_TRANSITION.include?(k)}
256 else
256 else
257 return
257 return
258 end
258 end
259
259
260 # Tracker must be set before since new_statuses_allowed_to depends on it.
260 # Tracker must be set before since new_statuses_allowed_to depends on it.
261 if t = attrs.delete('tracker_id')
261 if t = attrs.delete('tracker_id')
262 self.tracker_id = t
262 self.tracker_id = t
263 end
263 end
264
264
265 if attrs['status_id']
265 if attrs['status_id']
266 unless new_statuses_allowed_to(user).collect(&:id).include?(attrs['status_id'].to_i)
266 unless new_statuses_allowed_to(user).collect(&:id).include?(attrs['status_id'].to_i)
267 attrs.delete('status_id')
267 attrs.delete('status_id')
268 end
268 end
269 end
269 end
270
270
271 unless leaf?
271 unless leaf?
272 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
272 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
273 end
273 end
274
274
275 if attrs.has_key?('parent_issue_id')
275 if attrs.has_key?('parent_issue_id')
276 if !user.allowed_to?(:manage_subtasks, project)
276 if !user.allowed_to?(:manage_subtasks, project)
277 attrs.delete('parent_issue_id')
277 attrs.delete('parent_issue_id')
278 elsif !attrs['parent_issue_id'].blank?
278 elsif !attrs['parent_issue_id'].blank?
279 attrs.delete('parent_issue_id') unless Issue.visible(user).exists?(attrs['parent_issue_id'])
279 attrs.delete('parent_issue_id') unless Issue.visible(user).exists?(attrs['parent_issue_id'].to_i)
280 end
280 end
281 end
281 end
282
282
283 self.attributes = attrs
283 self.attributes = attrs
284 end
284 end
285
285
286 def done_ratio
286 def done_ratio
287 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
287 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
288 status.default_done_ratio
288 status.default_done_ratio
289 else
289 else
290 read_attribute(:done_ratio)
290 read_attribute(:done_ratio)
291 end
291 end
292 end
292 end
293
293
294 def self.use_status_for_done_ratio?
294 def self.use_status_for_done_ratio?
295 Setting.issue_done_ratio == 'issue_status'
295 Setting.issue_done_ratio == 'issue_status'
296 end
296 end
297
297
298 def self.use_field_for_done_ratio?
298 def self.use_field_for_done_ratio?
299 Setting.issue_done_ratio == 'issue_field'
299 Setting.issue_done_ratio == 'issue_field'
300 end
300 end
301
301
302 def validate
302 def validate
303 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
303 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
304 errors.add :due_date, :not_a_date
304 errors.add :due_date, :not_a_date
305 end
305 end
306
306
307 if self.due_date and self.start_date and self.due_date < self.start_date
307 if self.due_date and self.start_date and self.due_date < self.start_date
308 errors.add :due_date, :greater_than_start_date
308 errors.add :due_date, :greater_than_start_date
309 end
309 end
310
310
311 if start_date && soonest_start && start_date < soonest_start
311 if start_date && soonest_start && start_date < soonest_start
312 errors.add :start_date, :invalid
312 errors.add :start_date, :invalid
313 end
313 end
314
314
315 if fixed_version
315 if fixed_version
316 if !assignable_versions.include?(fixed_version)
316 if !assignable_versions.include?(fixed_version)
317 errors.add :fixed_version_id, :inclusion
317 errors.add :fixed_version_id, :inclusion
318 elsif reopened? && fixed_version.closed?
318 elsif reopened? && fixed_version.closed?
319 errors.add_to_base I18n.t(:error_can_not_reopen_issue_on_closed_version)
319 errors.add_to_base I18n.t(:error_can_not_reopen_issue_on_closed_version)
320 end
320 end
321 end
321 end
322
322
323 # Checks that the issue can not be added/moved to a disabled tracker
323 # Checks that the issue can not be added/moved to a disabled tracker
324 if project && (tracker_id_changed? || project_id_changed?)
324 if project && (tracker_id_changed? || project_id_changed?)
325 unless project.trackers.include?(tracker)
325 unless project.trackers.include?(tracker)
326 errors.add :tracker_id, :inclusion
326 errors.add :tracker_id, :inclusion
327 end
327 end
328 end
328 end
329
329
330 # Checks parent issue assignment
330 # Checks parent issue assignment
331 if @parent_issue
331 if @parent_issue
332 if @parent_issue.project_id != project_id
332 if @parent_issue.project_id != project_id
333 errors.add :parent_issue_id, :not_same_project
333 errors.add :parent_issue_id, :not_same_project
334 elsif !new_record?
334 elsif !new_record?
335 # moving an existing issue
335 # moving an existing issue
336 if @parent_issue.root_id != root_id
336 if @parent_issue.root_id != root_id
337 # we can always move to another tree
337 # we can always move to another tree
338 elsif move_possible?(@parent_issue)
338 elsif move_possible?(@parent_issue)
339 # move accepted inside tree
339 # move accepted inside tree
340 else
340 else
341 errors.add :parent_issue_id, :not_a_valid_parent
341 errors.add :parent_issue_id, :not_a_valid_parent
342 end
342 end
343 end
343 end
344 end
344 end
345 end
345 end
346
346
347 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
347 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
348 # even if the user turns off the setting later
348 # even if the user turns off the setting later
349 def update_done_ratio_from_issue_status
349 def update_done_ratio_from_issue_status
350 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
350 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
351 self.done_ratio = status.default_done_ratio
351 self.done_ratio = status.default_done_ratio
352 end
352 end
353 end
353 end
354
354
355 def init_journal(user, notes = "")
355 def init_journal(user, notes = "")
356 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
356 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
357 @issue_before_change = self.clone
357 @issue_before_change = self.clone
358 @issue_before_change.status = self.status
358 @issue_before_change.status = self.status
359 @custom_values_before_change = {}
359 @custom_values_before_change = {}
360 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
360 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
361 # Make sure updated_on is updated when adding a note.
361 # Make sure updated_on is updated when adding a note.
362 updated_on_will_change!
362 updated_on_will_change!
363 @current_journal
363 @current_journal
364 end
364 end
365
365
366 # Return true if the issue is closed, otherwise false
366 # Return true if the issue is closed, otherwise false
367 def closed?
367 def closed?
368 self.status.is_closed?
368 self.status.is_closed?
369 end
369 end
370
370
371 # Return true if the issue is being reopened
371 # Return true if the issue is being reopened
372 def reopened?
372 def reopened?
373 if !new_record? && status_id_changed?
373 if !new_record? && status_id_changed?
374 status_was = IssueStatus.find_by_id(status_id_was)
374 status_was = IssueStatus.find_by_id(status_id_was)
375 status_new = IssueStatus.find_by_id(status_id)
375 status_new = IssueStatus.find_by_id(status_id)
376 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
376 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
377 return true
377 return true
378 end
378 end
379 end
379 end
380 false
380 false
381 end
381 end
382
382
383 # Return true if the issue is being closed
383 # Return true if the issue is being closed
384 def closing?
384 def closing?
385 if !new_record? && status_id_changed?
385 if !new_record? && status_id_changed?
386 status_was = IssueStatus.find_by_id(status_id_was)
386 status_was = IssueStatus.find_by_id(status_id_was)
387 status_new = IssueStatus.find_by_id(status_id)
387 status_new = IssueStatus.find_by_id(status_id)
388 if status_was && status_new && !status_was.is_closed? && status_new.is_closed?
388 if status_was && status_new && !status_was.is_closed? && status_new.is_closed?
389 return true
389 return true
390 end
390 end
391 end
391 end
392 false
392 false
393 end
393 end
394
394
395 # Returns true if the issue is overdue
395 # Returns true if the issue is overdue
396 def overdue?
396 def overdue?
397 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
397 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
398 end
398 end
399
399
400 # Is the amount of work done less than it should for the due date
400 # Is the amount of work done less than it should for the due date
401 def behind_schedule?
401 def behind_schedule?
402 return false if start_date.nil? || due_date.nil?
402 return false if start_date.nil? || due_date.nil?
403 done_date = start_date + ((due_date - start_date+1)* done_ratio/100).floor
403 done_date = start_date + ((due_date - start_date+1)* done_ratio/100).floor
404 return done_date <= Date.today
404 return done_date <= Date.today
405 end
405 end
406
406
407 # Does this issue have children?
407 # Does this issue have children?
408 def children?
408 def children?
409 !leaf?
409 !leaf?
410 end
410 end
411
411
412 # Users the issue can be assigned to
412 # Users the issue can be assigned to
413 def assignable_users
413 def assignable_users
414 users = project.assignable_users
414 users = project.assignable_users
415 users << author if author
415 users << author if author
416 users.uniq.sort
416 users.uniq.sort
417 end
417 end
418
418
419 # Versions that the issue can be assigned to
419 # Versions that the issue can be assigned to
420 def assignable_versions
420 def assignable_versions
421 @assignable_versions ||= (project.shared_versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort
421 @assignable_versions ||= (project.shared_versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort
422 end
422 end
423
423
424 # Returns true if this issue is blocked by another issue that is still open
424 # Returns true if this issue is blocked by another issue that is still open
425 def blocked?
425 def blocked?
426 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
426 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
427 end
427 end
428
428
429 # Returns an array of status that user is able to apply
429 # Returns an array of status that user is able to apply
430 def new_statuses_allowed_to(user, include_default=false)
430 def new_statuses_allowed_to(user, include_default=false)
431 statuses = status.find_new_statuses_allowed_to(user.roles_for_project(project), tracker)
431 statuses = status.find_new_statuses_allowed_to(user.roles_for_project(project), tracker)
432 statuses << status unless statuses.empty?
432 statuses << status unless statuses.empty?
433 statuses << IssueStatus.default if include_default
433 statuses << IssueStatus.default if include_default
434 statuses = statuses.uniq.sort
434 statuses = statuses.uniq.sort
435 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
435 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
436 end
436 end
437
437
438 # Returns the mail adresses of users that should be notified
438 # Returns the mail adresses of users that should be notified
439 def recipients
439 def recipients
440 notified = project.notified_users
440 notified = project.notified_users
441 # Author and assignee are always notified unless they have been
441 # Author and assignee are always notified unless they have been
442 # locked or don't want to be notified
442 # locked or don't want to be notified
443 notified << author if author && author.active? && author.notify_about?(self)
443 notified << author if author && author.active? && author.notify_about?(self)
444 notified << assigned_to if assigned_to && assigned_to.active? && assigned_to.notify_about?(self)
444 notified << assigned_to if assigned_to && assigned_to.active? && assigned_to.notify_about?(self)
445 notified.uniq!
445 notified.uniq!
446 # Remove users that can not view the issue
446 # Remove users that can not view the issue
447 notified.reject! {|user| !visible?(user)}
447 notified.reject! {|user| !visible?(user)}
448 notified.collect(&:mail)
448 notified.collect(&:mail)
449 end
449 end
450
450
451 # Returns the total number of hours spent on this issue and its descendants
451 # Returns the total number of hours spent on this issue and its descendants
452 #
452 #
453 # Example:
453 # Example:
454 # spent_hours => 0.0
454 # spent_hours => 0.0
455 # spent_hours => 50.2
455 # spent_hours => 50.2
456 def spent_hours
456 def spent_hours
457 @spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours", :include => :time_entries).to_f || 0.0
457 @spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours", :include => :time_entries).to_f || 0.0
458 end
458 end
459
459
460 def relations
460 def relations
461 (relations_from + relations_to).sort
461 (relations_from + relations_to).sort
462 end
462 end
463
463
464 def all_dependent_issues
464 def all_dependent_issues
465 dependencies = []
465 dependencies = []
466 relations_from.each do |relation|
466 relations_from.each do |relation|
467 dependencies << relation.issue_to
467 dependencies << relation.issue_to
468 dependencies += relation.issue_to.all_dependent_issues
468 dependencies += relation.issue_to.all_dependent_issues
469 end
469 end
470 dependencies
470 dependencies
471 end
471 end
472
472
473 # Returns an array of issues that duplicate this one
473 # Returns an array of issues that duplicate this one
474 def duplicates
474 def duplicates
475 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
475 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
476 end
476 end
477
477
478 # Returns the due date or the target due date if any
478 # Returns the due date or the target due date if any
479 # Used on gantt chart
479 # Used on gantt chart
480 def due_before
480 def due_before
481 due_date || (fixed_version ? fixed_version.effective_date : nil)
481 due_date || (fixed_version ? fixed_version.effective_date : nil)
482 end
482 end
483
483
484 # Returns the time scheduled for this issue.
484 # Returns the time scheduled for this issue.
485 #
485 #
486 # Example:
486 # Example:
487 # Start Date: 2/26/09, End Date: 3/04/09
487 # Start Date: 2/26/09, End Date: 3/04/09
488 # duration => 6
488 # duration => 6
489 def duration
489 def duration
490 (start_date && due_date) ? due_date - start_date : 0
490 (start_date && due_date) ? due_date - start_date : 0
491 end
491 end
492
492
493 def soonest_start
493 def soonest_start
494 @soonest_start ||= (
494 @soonest_start ||= (
495 relations_to.collect{|relation| relation.successor_soonest_start} +
495 relations_to.collect{|relation| relation.successor_soonest_start} +
496 ancestors.collect(&:soonest_start)
496 ancestors.collect(&:soonest_start)
497 ).compact.max
497 ).compact.max
498 end
498 end
499
499
500 def reschedule_after(date)
500 def reschedule_after(date)
501 return if date.nil?
501 return if date.nil?
502 if leaf?
502 if leaf?
503 if start_date.nil? || start_date < date
503 if start_date.nil? || start_date < date
504 self.start_date, self.due_date = date, date + duration
504 self.start_date, self.due_date = date, date + duration
505 save
505 save
506 end
506 end
507 else
507 else
508 leaves.each do |leaf|
508 leaves.each do |leaf|
509 leaf.reschedule_after(date)
509 leaf.reschedule_after(date)
510 end
510 end
511 end
511 end
512 end
512 end
513
513
514 def <=>(issue)
514 def <=>(issue)
515 if issue.nil?
515 if issue.nil?
516 -1
516 -1
517 elsif root_id != issue.root_id
517 elsif root_id != issue.root_id
518 (root_id || 0) <=> (issue.root_id || 0)
518 (root_id || 0) <=> (issue.root_id || 0)
519 else
519 else
520 (lft || 0) <=> (issue.lft || 0)
520 (lft || 0) <=> (issue.lft || 0)
521 end
521 end
522 end
522 end
523
523
524 def to_s
524 def to_s
525 "#{tracker} ##{id}: #{subject}"
525 "#{tracker} ##{id}: #{subject}"
526 end
526 end
527
527
528 # Returns a string of css classes that apply to the issue
528 # Returns a string of css classes that apply to the issue
529 def css_classes
529 def css_classes
530 s = "issue status-#{status.position} priority-#{priority.position}"
530 s = "issue status-#{status.position} priority-#{priority.position}"
531 s << ' closed' if closed?
531 s << ' closed' if closed?
532 s << ' overdue' if overdue?
532 s << ' overdue' if overdue?
533 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
533 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
534 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
534 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
535 s
535 s
536 end
536 end
537
537
538 # Saves an issue, time_entry, attachments, and a journal from the parameters
538 # Saves an issue, time_entry, attachments, and a journal from the parameters
539 # Returns false if save fails
539 # Returns false if save fails
540 def save_issue_with_child_records(params, existing_time_entry=nil)
540 def save_issue_with_child_records(params, existing_time_entry=nil)
541 Issue.transaction do
541 Issue.transaction do
542 if params[:time_entry] && params[:time_entry][:hours].present? && User.current.allowed_to?(:log_time, project)
542 if params[:time_entry] && params[:time_entry][:hours].present? && User.current.allowed_to?(:log_time, project)
543 @time_entry = existing_time_entry || TimeEntry.new
543 @time_entry = existing_time_entry || TimeEntry.new
544 @time_entry.project = project
544 @time_entry.project = project
545 @time_entry.issue = self
545 @time_entry.issue = self
546 @time_entry.user = User.current
546 @time_entry.user = User.current
547 @time_entry.spent_on = Date.today
547 @time_entry.spent_on = Date.today
548 @time_entry.attributes = params[:time_entry]
548 @time_entry.attributes = params[:time_entry]
549 self.time_entries << @time_entry
549 self.time_entries << @time_entry
550 end
550 end
551
551
552 if valid?
552 if valid?
553 attachments = Attachment.attach_files(self, params[:attachments])
553 attachments = Attachment.attach_files(self, params[:attachments])
554
554
555 attachments[:files].each {|a| @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
555 attachments[:files].each {|a| @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
556 # TODO: Rename hook
556 # TODO: Rename hook
557 Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
557 Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
558 begin
558 begin
559 if save
559 if save
560 # TODO: Rename hook
560 # TODO: Rename hook
561 Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
561 Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
562 else
562 else
563 raise ActiveRecord::Rollback
563 raise ActiveRecord::Rollback
564 end
564 end
565 rescue ActiveRecord::StaleObjectError
565 rescue ActiveRecord::StaleObjectError
566 attachments[:files].each(&:destroy)
566 attachments[:files].each(&:destroy)
567 errors.add_to_base l(:notice_locking_conflict)
567 errors.add_to_base l(:notice_locking_conflict)
568 raise ActiveRecord::Rollback
568 raise ActiveRecord::Rollback
569 end
569 end
570 end
570 end
571 end
571 end
572 end
572 end
573
573
574 # Unassigns issues from +version+ if it's no longer shared with issue's project
574 # Unassigns issues from +version+ if it's no longer shared with issue's project
575 def self.update_versions_from_sharing_change(version)
575 def self.update_versions_from_sharing_change(version)
576 # Update issues assigned to the version
576 # Update issues assigned to the version
577 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
577 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
578 end
578 end
579
579
580 # Unassigns issues from versions that are no longer shared
580 # Unassigns issues from versions that are no longer shared
581 # after +project+ was moved
581 # after +project+ was moved
582 def self.update_versions_from_hierarchy_change(project)
582 def self.update_versions_from_hierarchy_change(project)
583 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
583 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
584 # Update issues of the moved projects and issues assigned to a version of a moved project
584 # Update issues of the moved projects and issues assigned to a version of a moved project
585 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
585 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
586 end
586 end
587
587
588 def parent_issue_id=(arg)
588 def parent_issue_id=(arg)
589 parent_issue_id = arg.blank? ? nil : arg.to_i
589 parent_issue_id = arg.blank? ? nil : arg.to_i
590 if parent_issue_id && @parent_issue = Issue.find_by_id(parent_issue_id)
590 if parent_issue_id && @parent_issue = Issue.find_by_id(parent_issue_id)
591 @parent_issue.id
591 @parent_issue.id
592 else
592 else
593 @parent_issue = nil
593 @parent_issue = nil
594 nil
594 nil
595 end
595 end
596 end
596 end
597
597
598 def parent_issue_id
598 def parent_issue_id
599 if instance_variable_defined? :@parent_issue
599 if instance_variable_defined? :@parent_issue
600 @parent_issue.nil? ? nil : @parent_issue.id
600 @parent_issue.nil? ? nil : @parent_issue.id
601 else
601 else
602 parent_id
602 parent_id
603 end
603 end
604 end
604 end
605
605
606 # Extracted from the ReportsController.
606 # Extracted from the ReportsController.
607 def self.by_tracker(project)
607 def self.by_tracker(project)
608 count_and_group_by(:project => project,
608 count_and_group_by(:project => project,
609 :field => 'tracker_id',
609 :field => 'tracker_id',
610 :joins => Tracker.table_name)
610 :joins => Tracker.table_name)
611 end
611 end
612
612
613 def self.by_version(project)
613 def self.by_version(project)
614 count_and_group_by(:project => project,
614 count_and_group_by(:project => project,
615 :field => 'fixed_version_id',
615 :field => 'fixed_version_id',
616 :joins => Version.table_name)
616 :joins => Version.table_name)
617 end
617 end
618
618
619 def self.by_priority(project)
619 def self.by_priority(project)
620 count_and_group_by(:project => project,
620 count_and_group_by(:project => project,
621 :field => 'priority_id',
621 :field => 'priority_id',
622 :joins => IssuePriority.table_name)
622 :joins => IssuePriority.table_name)
623 end
623 end
624
624
625 def self.by_category(project)
625 def self.by_category(project)
626 count_and_group_by(:project => project,
626 count_and_group_by(:project => project,
627 :field => 'category_id',
627 :field => 'category_id',
628 :joins => IssueCategory.table_name)
628 :joins => IssueCategory.table_name)
629 end
629 end
630
630
631 def self.by_assigned_to(project)
631 def self.by_assigned_to(project)
632 count_and_group_by(:project => project,
632 count_and_group_by(:project => project,
633 :field => 'assigned_to_id',
633 :field => 'assigned_to_id',
634 :joins => User.table_name)
634 :joins => User.table_name)
635 end
635 end
636
636
637 def self.by_author(project)
637 def self.by_author(project)
638 count_and_group_by(:project => project,
638 count_and_group_by(:project => project,
639 :field => 'author_id',
639 :field => 'author_id',
640 :joins => User.table_name)
640 :joins => User.table_name)
641 end
641 end
642
642
643 def self.by_subproject(project)
643 def self.by_subproject(project)
644 ActiveRecord::Base.connection.select_all("select s.id as status_id,
644 ActiveRecord::Base.connection.select_all("select s.id as status_id,
645 s.is_closed as closed,
645 s.is_closed as closed,
646 i.project_id as project_id,
646 i.project_id as project_id,
647 count(i.id) as total
647 count(i.id) as total
648 from
648 from
649 #{Issue.table_name} i, #{IssueStatus.table_name} s
649 #{Issue.table_name} i, #{IssueStatus.table_name} s
650 where
650 where
651 i.status_id=s.id
651 i.status_id=s.id
652 and i.project_id IN (#{project.descendants.active.collect{|p| p.id}.join(',')})
652 and i.project_id IN (#{project.descendants.active.collect{|p| p.id}.join(',')})
653 group by s.id, s.is_closed, i.project_id") if project.descendants.active.any?
653 group by s.id, s.is_closed, i.project_id") if project.descendants.active.any?
654 end
654 end
655 # End ReportsController extraction
655 # End ReportsController extraction
656
656
657 # Returns an array of projects that current user can move issues to
657 # Returns an array of projects that current user can move issues to
658 def self.allowed_target_projects_on_move
658 def self.allowed_target_projects_on_move
659 projects = []
659 projects = []
660 if User.current.admin?
660 if User.current.admin?
661 # admin is allowed to move issues to any active (visible) project
661 # admin is allowed to move issues to any active (visible) project
662 projects = Project.visible.all
662 projects = Project.visible.all
663 elsif User.current.logged?
663 elsif User.current.logged?
664 if Role.non_member.allowed_to?(:move_issues)
664 if Role.non_member.allowed_to?(:move_issues)
665 projects = Project.visible.all
665 projects = Project.visible.all
666 else
666 else
667 User.current.memberships.each {|m| projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}}
667 User.current.memberships.each {|m| projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}}
668 end
668 end
669 end
669 end
670 projects
670 projects
671 end
671 end
672
672
673 private
673 private
674
674
675 def update_nested_set_attributes
675 def update_nested_set_attributes
676 if root_id.nil?
676 if root_id.nil?
677 # issue was just created
677 # issue was just created
678 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
678 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
679 set_default_left_and_right
679 set_default_left_and_right
680 Issue.update_all("root_id = #{root_id}, lft = #{lft}, rgt = #{rgt}", ["id = ?", id])
680 Issue.update_all("root_id = #{root_id}, lft = #{lft}, rgt = #{rgt}", ["id = ?", id])
681 if @parent_issue
681 if @parent_issue
682 move_to_child_of(@parent_issue)
682 move_to_child_of(@parent_issue)
683 end
683 end
684 reload
684 reload
685 elsif parent_issue_id != parent_id
685 elsif parent_issue_id != parent_id
686 former_parent_id = parent_id
686 former_parent_id = parent_id
687 # moving an existing issue
687 # moving an existing issue
688 if @parent_issue && @parent_issue.root_id == root_id
688 if @parent_issue && @parent_issue.root_id == root_id
689 # inside the same tree
689 # inside the same tree
690 move_to_child_of(@parent_issue)
690 move_to_child_of(@parent_issue)
691 else
691 else
692 # to another tree
692 # to another tree
693 unless root?
693 unless root?
694 move_to_right_of(root)
694 move_to_right_of(root)
695 reload
695 reload
696 end
696 end
697 old_root_id = root_id
697 old_root_id = root_id
698 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
698 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
699 target_maxright = nested_set_scope.maximum(right_column_name) || 0
699 target_maxright = nested_set_scope.maximum(right_column_name) || 0
700 offset = target_maxright + 1 - lft
700 offset = target_maxright + 1 - lft
701 Issue.update_all("root_id = #{root_id}, lft = lft + #{offset}, rgt = rgt + #{offset}",
701 Issue.update_all("root_id = #{root_id}, lft = lft + #{offset}, rgt = rgt + #{offset}",
702 ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
702 ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
703 self[left_column_name] = lft + offset
703 self[left_column_name] = lft + offset
704 self[right_column_name] = rgt + offset
704 self[right_column_name] = rgt + offset
705 if @parent_issue
705 if @parent_issue
706 move_to_child_of(@parent_issue)
706 move_to_child_of(@parent_issue)
707 end
707 end
708 end
708 end
709 reload
709 reload
710 # delete invalid relations of all descendants
710 # delete invalid relations of all descendants
711 self_and_descendants.each do |issue|
711 self_and_descendants.each do |issue|
712 issue.relations.each do |relation|
712 issue.relations.each do |relation|
713 relation.destroy unless relation.valid?
713 relation.destroy unless relation.valid?
714 end
714 end
715 end
715 end
716 # update former parent
716 # update former parent
717 recalculate_attributes_for(former_parent_id) if former_parent_id
717 recalculate_attributes_for(former_parent_id) if former_parent_id
718 end
718 end
719 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
719 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
720 end
720 end
721
721
722 def update_parent_attributes
722 def update_parent_attributes
723 recalculate_attributes_for(parent_id) if parent_id
723 recalculate_attributes_for(parent_id) if parent_id
724 end
724 end
725
725
726 def recalculate_attributes_for(issue_id)
726 def recalculate_attributes_for(issue_id)
727 if issue_id && p = Issue.find_by_id(issue_id)
727 if issue_id && p = Issue.find_by_id(issue_id)
728 # priority = highest priority of children
728 # priority = highest priority of children
729 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :include => :priority)
729 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :include => :priority)
730 p.priority = IssuePriority.find_by_position(priority_position)
730 p.priority = IssuePriority.find_by_position(priority_position)
731 end
731 end
732
732
733 # start/due dates = lowest/highest dates of children
733 # start/due dates = lowest/highest dates of children
734 p.start_date = p.children.minimum(:start_date)
734 p.start_date = p.children.minimum(:start_date)
735 p.due_date = p.children.maximum(:due_date)
735 p.due_date = p.children.maximum(:due_date)
736 if p.start_date && p.due_date && p.due_date < p.start_date
736 if p.start_date && p.due_date && p.due_date < p.start_date
737 p.start_date, p.due_date = p.due_date, p.start_date
737 p.start_date, p.due_date = p.due_date, p.start_date
738 end
738 end
739
739
740 # done ratio = weighted average ratio of leaves
740 # done ratio = weighted average ratio of leaves
741 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
741 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
742 leaves_count = p.leaves.count
742 leaves_count = p.leaves.count
743 if leaves_count > 0
743 if leaves_count > 0
744 average = p.leaves.average(:estimated_hours).to_f
744 average = p.leaves.average(:estimated_hours).to_f
745 if average == 0
745 if average == 0
746 average = 1
746 average = 1
747 end
747 end
748 done = p.leaves.sum("COALESCE(estimated_hours, #{average}) * (CASE WHEN is_closed = #{connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)", :include => :status).to_f
748 done = p.leaves.sum("COALESCE(estimated_hours, #{average}) * (CASE WHEN is_closed = #{connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)", :include => :status).to_f
749 progress = done / (average * leaves_count)
749 progress = done / (average * leaves_count)
750 p.done_ratio = progress.round
750 p.done_ratio = progress.round
751 end
751 end
752 end
752 end
753
753
754 # estimate = sum of leaves estimates
754 # estimate = sum of leaves estimates
755 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
755 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
756 p.estimated_hours = nil if p.estimated_hours == 0.0
756 p.estimated_hours = nil if p.estimated_hours == 0.0
757
757
758 # ancestors will be recursively updated
758 # ancestors will be recursively updated
759 p.save(false)
759 p.save(false)
760 end
760 end
761 end
761 end
762
762
763 def destroy_children
763 def destroy_children
764 unless leaf?
764 unless leaf?
765 children.each do |child|
765 children.each do |child|
766 child.destroy
766 child.destroy
767 end
767 end
768 end
768 end
769 end
769 end
770
770
771 # Update issues so their versions are not pointing to a
771 # Update issues so their versions are not pointing to a
772 # fixed_version that is not shared with the issue's project
772 # fixed_version that is not shared with the issue's project
773 def self.update_versions(conditions=nil)
773 def self.update_versions(conditions=nil)
774 # Only need to update issues with a fixed_version from
774 # Only need to update issues with a fixed_version from
775 # a different project and that is not systemwide shared
775 # a different project and that is not systemwide shared
776 Issue.all(:conditions => merge_conditions("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
776 Issue.all(:conditions => merge_conditions("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
777 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
777 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
778 " AND #{Version.table_name}.sharing <> 'system'",
778 " AND #{Version.table_name}.sharing <> 'system'",
779 conditions),
779 conditions),
780 :include => [:project, :fixed_version]
780 :include => [:project, :fixed_version]
781 ).each do |issue|
781 ).each do |issue|
782 next if issue.project.nil? || issue.fixed_version.nil?
782 next if issue.project.nil? || issue.fixed_version.nil?
783 unless issue.project.shared_versions.include?(issue.fixed_version)
783 unless issue.project.shared_versions.include?(issue.fixed_version)
784 issue.init_journal(User.current)
784 issue.init_journal(User.current)
785 issue.fixed_version = nil
785 issue.fixed_version = nil
786 issue.save
786 issue.save
787 end
787 end
788 end
788 end
789 end
789 end
790
790
791 # Callback on attachment deletion
791 # Callback on attachment deletion
792 def attachment_removed(obj)
792 def attachment_removed(obj)
793 journal = init_journal(User.current)
793 journal = init_journal(User.current)
794 journal.details << JournalDetail.new(:property => 'attachment',
794 journal.details << JournalDetail.new(:property => 'attachment',
795 :prop_key => obj.id,
795 :prop_key => obj.id,
796 :old_value => obj.filename)
796 :old_value => obj.filename)
797 journal.save
797 journal.save
798 end
798 end
799
799
800 # Default assignment based on category
800 # Default assignment based on category
801 def default_assign
801 def default_assign
802 if assigned_to.nil? && category && category.assigned_to
802 if assigned_to.nil? && category && category.assigned_to
803 self.assigned_to = category.assigned_to
803 self.assigned_to = category.assigned_to
804 end
804 end
805 end
805 end
806
806
807 # Updates start/due dates of following issues
807 # Updates start/due dates of following issues
808 def reschedule_following_issues
808 def reschedule_following_issues
809 if start_date_changed? || due_date_changed?
809 if start_date_changed? || due_date_changed?
810 relations_from.each do |relation|
810 relations_from.each do |relation|
811 relation.set_issue_to_dates
811 relation.set_issue_to_dates
812 end
812 end
813 end
813 end
814 end
814 end
815
815
816 # Closes duplicates if the issue is being closed
816 # Closes duplicates if the issue is being closed
817 def close_duplicates
817 def close_duplicates
818 if closing?
818 if closing?
819 duplicates.each do |duplicate|
819 duplicates.each do |duplicate|
820 # Reload is need in case the duplicate was updated by a previous duplicate
820 # Reload is need in case the duplicate was updated by a previous duplicate
821 duplicate.reload
821 duplicate.reload
822 # Don't re-close it if it's already closed
822 # Don't re-close it if it's already closed
823 next if duplicate.closed?
823 next if duplicate.closed?
824 # Same user and notes
824 # Same user and notes
825 if @current_journal
825 if @current_journal
826 duplicate.init_journal(@current_journal.user, @current_journal.notes)
826 duplicate.init_journal(@current_journal.user, @current_journal.notes)
827 end
827 end
828 duplicate.update_attribute :status, self.status
828 duplicate.update_attribute :status, self.status
829 end
829 end
830 end
830 end
831 end
831 end
832
832
833 # Saves the changes in a Journal
833 # Saves the changes in a Journal
834 # Called after_save
834 # Called after_save
835 def create_journal
835 def create_journal
836 if @current_journal
836 if @current_journal
837 # attributes changes
837 # attributes changes
838 (Issue.column_names - %w(id description root_id lft rgt lock_version created_on updated_on)).each {|c|
838 (Issue.column_names - %w(id description root_id lft rgt lock_version created_on updated_on)).each {|c|
839 @current_journal.details << JournalDetail.new(:property => 'attr',
839 @current_journal.details << JournalDetail.new(:property => 'attr',
840 :prop_key => c,
840 :prop_key => c,
841 :old_value => @issue_before_change.send(c),
841 :old_value => @issue_before_change.send(c),
842 :value => send(c)) unless send(c)==@issue_before_change.send(c)
842 :value => send(c)) unless send(c)==@issue_before_change.send(c)
843 }
843 }
844 # custom fields changes
844 # custom fields changes
845 custom_values.each {|c|
845 custom_values.each {|c|
846 next if (@custom_values_before_change[c.custom_field_id]==c.value ||
846 next if (@custom_values_before_change[c.custom_field_id]==c.value ||
847 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
847 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
848 @current_journal.details << JournalDetail.new(:property => 'cf',
848 @current_journal.details << JournalDetail.new(:property => 'cf',
849 :prop_key => c.custom_field_id,
849 :prop_key => c.custom_field_id,
850 :old_value => @custom_values_before_change[c.custom_field_id],
850 :old_value => @custom_values_before_change[c.custom_field_id],
851 :value => c.value)
851 :value => c.value)
852 }
852 }
853 @current_journal.save
853 @current_journal.save
854 # reset current journal
854 # reset current journal
855 init_journal @current_journal.user, @current_journal.notes
855 init_journal @current_journal.user, @current_journal.notes
856 end
856 end
857 end
857 end
858
858
859 # Query generator for selecting groups of issue counts for a project
859 # Query generator for selecting groups of issue counts for a project
860 # based on specific criteria
860 # based on specific criteria
861 #
861 #
862 # Options
862 # Options
863 # * project - Project to search in.
863 # * project - Project to search in.
864 # * field - String. Issue field to key off of in the grouping.
864 # * field - String. Issue field to key off of in the grouping.
865 # * joins - String. The table name to join against.
865 # * joins - String. The table name to join against.
866 def self.count_and_group_by(options)
866 def self.count_and_group_by(options)
867 project = options.delete(:project)
867 project = options.delete(:project)
868 select_field = options.delete(:field)
868 select_field = options.delete(:field)
869 joins = options.delete(:joins)
869 joins = options.delete(:joins)
870
870
871 where = "i.#{select_field}=j.id"
871 where = "i.#{select_field}=j.id"
872
872
873 ActiveRecord::Base.connection.select_all("select s.id as status_id,
873 ActiveRecord::Base.connection.select_all("select s.id as status_id,
874 s.is_closed as closed,
874 s.is_closed as closed,
875 j.id as #{select_field},
875 j.id as #{select_field},
876 count(i.id) as total
876 count(i.id) as total
877 from
877 from
878 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{joins} j
878 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{joins} j
879 where
879 where
880 i.status_id=s.id
880 i.status_id=s.id
881 and #{where}
881 and #{where}
882 and i.project_id=#{project.id}
882 and i.project_id=#{project.id}
883 group by s.id, s.is_closed, j.id")
883 group by s.id, s.is_closed, j.id")
884 end
884 end
885
885
886
886
887 end
887 end
@@ -1,1278 +1,1292
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 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.dirname(__FILE__) + '/../test_helper'
18 require File.dirname(__FILE__) + '/../test_helper'
19 require 'issues_controller'
19 require 'issues_controller'
20
20
21 # Re-raise errors caught by the controller.
21 # Re-raise errors caught by the controller.
22 class IssuesController; def rescue_action(e) raise e end; end
22 class IssuesController; def rescue_action(e) raise e end; end
23
23
24 class IssuesControllerTest < ActionController::TestCase
24 class IssuesControllerTest < ActionController::TestCase
25 fixtures :projects,
25 fixtures :projects,
26 :users,
26 :users,
27 :roles,
27 :roles,
28 :members,
28 :members,
29 :member_roles,
29 :member_roles,
30 :issues,
30 :issues,
31 :issue_statuses,
31 :issue_statuses,
32 :versions,
32 :versions,
33 :trackers,
33 :trackers,
34 :projects_trackers,
34 :projects_trackers,
35 :issue_categories,
35 :issue_categories,
36 :enabled_modules,
36 :enabled_modules,
37 :enumerations,
37 :enumerations,
38 :attachments,
38 :attachments,
39 :workflows,
39 :workflows,
40 :custom_fields,
40 :custom_fields,
41 :custom_values,
41 :custom_values,
42 :custom_fields_projects,
42 :custom_fields_projects,
43 :custom_fields_trackers,
43 :custom_fields_trackers,
44 :time_entries,
44 :time_entries,
45 :journals,
45 :journals,
46 :journal_details,
46 :journal_details,
47 :queries
47 :queries
48
48
49 def setup
49 def setup
50 @controller = IssuesController.new
50 @controller = IssuesController.new
51 @request = ActionController::TestRequest.new
51 @request = ActionController::TestRequest.new
52 @response = ActionController::TestResponse.new
52 @response = ActionController::TestResponse.new
53 User.current = nil
53 User.current = nil
54 end
54 end
55
55
56 def test_index
56 def test_index
57 Setting.default_language = 'en'
57 Setting.default_language = 'en'
58
58
59 get :index
59 get :index
60 assert_response :success
60 assert_response :success
61 assert_template 'index.rhtml'
61 assert_template 'index.rhtml'
62 assert_not_nil assigns(:issues)
62 assert_not_nil assigns(:issues)
63 assert_nil assigns(:project)
63 assert_nil assigns(:project)
64 assert_tag :tag => 'a', :content => /Can't print recipes/
64 assert_tag :tag => 'a', :content => /Can't print recipes/
65 assert_tag :tag => 'a', :content => /Subproject issue/
65 assert_tag :tag => 'a', :content => /Subproject issue/
66 # private projects hidden
66 # private projects hidden
67 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
67 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
68 assert_no_tag :tag => 'a', :content => /Issue on project 2/
68 assert_no_tag :tag => 'a', :content => /Issue on project 2/
69 # project column
69 # project column
70 assert_tag :tag => 'th', :content => /Project/
70 assert_tag :tag => 'th', :content => /Project/
71 end
71 end
72
72
73 def test_index_should_not_list_issues_when_module_disabled
73 def test_index_should_not_list_issues_when_module_disabled
74 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
74 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
75 get :index
75 get :index
76 assert_response :success
76 assert_response :success
77 assert_template 'index.rhtml'
77 assert_template 'index.rhtml'
78 assert_not_nil assigns(:issues)
78 assert_not_nil assigns(:issues)
79 assert_nil assigns(:project)
79 assert_nil assigns(:project)
80 assert_no_tag :tag => 'a', :content => /Can't print recipes/
80 assert_no_tag :tag => 'a', :content => /Can't print recipes/
81 assert_tag :tag => 'a', :content => /Subproject issue/
81 assert_tag :tag => 'a', :content => /Subproject issue/
82 end
82 end
83
83
84 def test_index_should_not_list_issues_when_module_disabled
84 def test_index_should_not_list_issues_when_module_disabled
85 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
85 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
86 get :index
86 get :index
87 assert_response :success
87 assert_response :success
88 assert_template 'index.rhtml'
88 assert_template 'index.rhtml'
89 assert_not_nil assigns(:issues)
89 assert_not_nil assigns(:issues)
90 assert_nil assigns(:project)
90 assert_nil assigns(:project)
91 assert_no_tag :tag => 'a', :content => /Can't print recipes/
91 assert_no_tag :tag => 'a', :content => /Can't print recipes/
92 assert_tag :tag => 'a', :content => /Subproject issue/
92 assert_tag :tag => 'a', :content => /Subproject issue/
93 end
93 end
94
94
95 def test_index_with_project
95 def test_index_with_project
96 Setting.display_subprojects_issues = 0
96 Setting.display_subprojects_issues = 0
97 get :index, :project_id => 1
97 get :index, :project_id => 1
98 assert_response :success
98 assert_response :success
99 assert_template 'index.rhtml'
99 assert_template 'index.rhtml'
100 assert_not_nil assigns(:issues)
100 assert_not_nil assigns(:issues)
101 assert_tag :tag => 'a', :content => /Can't print recipes/
101 assert_tag :tag => 'a', :content => /Can't print recipes/
102 assert_no_tag :tag => 'a', :content => /Subproject issue/
102 assert_no_tag :tag => 'a', :content => /Subproject issue/
103 end
103 end
104
104
105 def test_index_with_project_and_subprojects
105 def test_index_with_project_and_subprojects
106 Setting.display_subprojects_issues = 1
106 Setting.display_subprojects_issues = 1
107 get :index, :project_id => 1
107 get :index, :project_id => 1
108 assert_response :success
108 assert_response :success
109 assert_template 'index.rhtml'
109 assert_template 'index.rhtml'
110 assert_not_nil assigns(:issues)
110 assert_not_nil assigns(:issues)
111 assert_tag :tag => 'a', :content => /Can't print recipes/
111 assert_tag :tag => 'a', :content => /Can't print recipes/
112 assert_tag :tag => 'a', :content => /Subproject issue/
112 assert_tag :tag => 'a', :content => /Subproject issue/
113 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
113 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
114 end
114 end
115
115
116 def test_index_with_project_and_subprojects_should_show_private_subprojects
116 def test_index_with_project_and_subprojects_should_show_private_subprojects
117 @request.session[:user_id] = 2
117 @request.session[:user_id] = 2
118 Setting.display_subprojects_issues = 1
118 Setting.display_subprojects_issues = 1
119 get :index, :project_id => 1
119 get :index, :project_id => 1
120 assert_response :success
120 assert_response :success
121 assert_template 'index.rhtml'
121 assert_template 'index.rhtml'
122 assert_not_nil assigns(:issues)
122 assert_not_nil assigns(:issues)
123 assert_tag :tag => 'a', :content => /Can't print recipes/
123 assert_tag :tag => 'a', :content => /Can't print recipes/
124 assert_tag :tag => 'a', :content => /Subproject issue/
124 assert_tag :tag => 'a', :content => /Subproject issue/
125 assert_tag :tag => 'a', :content => /Issue of a private subproject/
125 assert_tag :tag => 'a', :content => /Issue of a private subproject/
126 end
126 end
127
127
128 def test_index_with_project_and_default_filter
128 def test_index_with_project_and_default_filter
129 get :index, :project_id => 1, :set_filter => 1
129 get :index, :project_id => 1, :set_filter => 1
130 assert_response :success
130 assert_response :success
131 assert_template 'index.rhtml'
131 assert_template 'index.rhtml'
132 assert_not_nil assigns(:issues)
132 assert_not_nil assigns(:issues)
133
133
134 query = assigns(:query)
134 query = assigns(:query)
135 assert_not_nil query
135 assert_not_nil query
136 # default filter
136 # default filter
137 assert_equal({'status_id' => {:operator => 'o', :values => ['']}}, query.filters)
137 assert_equal({'status_id' => {:operator => 'o', :values => ['']}}, query.filters)
138 end
138 end
139
139
140 def test_index_with_project_and_filter
140 def test_index_with_project_and_filter
141 get :index, :project_id => 1, :set_filter => 1,
141 get :index, :project_id => 1, :set_filter => 1,
142 :fields => ['tracker_id'],
142 :fields => ['tracker_id'],
143 :operators => {'tracker_id' => '='},
143 :operators => {'tracker_id' => '='},
144 :values => {'tracker_id' => ['1']}
144 :values => {'tracker_id' => ['1']}
145 assert_response :success
145 assert_response :success
146 assert_template 'index.rhtml'
146 assert_template 'index.rhtml'
147 assert_not_nil assigns(:issues)
147 assert_not_nil assigns(:issues)
148
148
149 query = assigns(:query)
149 query = assigns(:query)
150 assert_not_nil query
150 assert_not_nil query
151 assert_equal({'tracker_id' => {:operator => '=', :values => ['1']}}, query.filters)
151 assert_equal({'tracker_id' => {:operator => '=', :values => ['1']}}, query.filters)
152 end
152 end
153
153
154 def test_index_with_project_and_empty_filters
154 def test_index_with_project_and_empty_filters
155 get :index, :project_id => 1, :set_filter => 1, :fields => ['']
155 get :index, :project_id => 1, :set_filter => 1, :fields => ['']
156 assert_response :success
156 assert_response :success
157 assert_template 'index.rhtml'
157 assert_template 'index.rhtml'
158 assert_not_nil assigns(:issues)
158 assert_not_nil assigns(:issues)
159
159
160 query = assigns(:query)
160 query = assigns(:query)
161 assert_not_nil query
161 assert_not_nil query
162 # no filter
162 # no filter
163 assert_equal({}, query.filters)
163 assert_equal({}, query.filters)
164 end
164 end
165
165
166 def test_index_with_query
166 def test_index_with_query
167 get :index, :project_id => 1, :query_id => 5
167 get :index, :project_id => 1, :query_id => 5
168 assert_response :success
168 assert_response :success
169 assert_template 'index.rhtml'
169 assert_template 'index.rhtml'
170 assert_not_nil assigns(:issues)
170 assert_not_nil assigns(:issues)
171 assert_nil assigns(:issue_count_by_group)
171 assert_nil assigns(:issue_count_by_group)
172 end
172 end
173
173
174 def test_index_with_query_grouped_by_tracker
174 def test_index_with_query_grouped_by_tracker
175 get :index, :project_id => 1, :query_id => 6
175 get :index, :project_id => 1, :query_id => 6
176 assert_response :success
176 assert_response :success
177 assert_template 'index.rhtml'
177 assert_template 'index.rhtml'
178 assert_not_nil assigns(:issues)
178 assert_not_nil assigns(:issues)
179 assert_not_nil assigns(:issue_count_by_group)
179 assert_not_nil assigns(:issue_count_by_group)
180 end
180 end
181
181
182 def test_index_with_query_grouped_by_list_custom_field
182 def test_index_with_query_grouped_by_list_custom_field
183 get :index, :project_id => 1, :query_id => 9
183 get :index, :project_id => 1, :query_id => 9
184 assert_response :success
184 assert_response :success
185 assert_template 'index.rhtml'
185 assert_template 'index.rhtml'
186 assert_not_nil assigns(:issues)
186 assert_not_nil assigns(:issues)
187 assert_not_nil assigns(:issue_count_by_group)
187 assert_not_nil assigns(:issue_count_by_group)
188 end
188 end
189
189
190 def test_index_sort_by_field_not_included_in_columns
190 def test_index_sort_by_field_not_included_in_columns
191 Setting.issue_list_default_columns = %w(subject author)
191 Setting.issue_list_default_columns = %w(subject author)
192 get :index, :sort => 'tracker'
192 get :index, :sort => 'tracker'
193 end
193 end
194
194
195 def test_index_csv_with_project
195 def test_index_csv_with_project
196 Setting.default_language = 'en'
196 Setting.default_language = 'en'
197
197
198 get :index, :format => 'csv'
198 get :index, :format => 'csv'
199 assert_response :success
199 assert_response :success
200 assert_not_nil assigns(:issues)
200 assert_not_nil assigns(:issues)
201 assert_equal 'text/csv', @response.content_type
201 assert_equal 'text/csv', @response.content_type
202 assert @response.body.starts_with?("#,")
202 assert @response.body.starts_with?("#,")
203
203
204 get :index, :project_id => 1, :format => 'csv'
204 get :index, :project_id => 1, :format => 'csv'
205 assert_response :success
205 assert_response :success
206 assert_not_nil assigns(:issues)
206 assert_not_nil assigns(:issues)
207 assert_equal 'text/csv', @response.content_type
207 assert_equal 'text/csv', @response.content_type
208 end
208 end
209
209
210 def test_index_pdf
210 def test_index_pdf
211 get :index, :format => 'pdf'
211 get :index, :format => 'pdf'
212 assert_response :success
212 assert_response :success
213 assert_not_nil assigns(:issues)
213 assert_not_nil assigns(:issues)
214 assert_equal 'application/pdf', @response.content_type
214 assert_equal 'application/pdf', @response.content_type
215
215
216 get :index, :project_id => 1, :format => 'pdf'
216 get :index, :project_id => 1, :format => 'pdf'
217 assert_response :success
217 assert_response :success
218 assert_not_nil assigns(:issues)
218 assert_not_nil assigns(:issues)
219 assert_equal 'application/pdf', @response.content_type
219 assert_equal 'application/pdf', @response.content_type
220
220
221 get :index, :project_id => 1, :query_id => 6, :format => 'pdf'
221 get :index, :project_id => 1, :query_id => 6, :format => 'pdf'
222 assert_response :success
222 assert_response :success
223 assert_not_nil assigns(:issues)
223 assert_not_nil assigns(:issues)
224 assert_equal 'application/pdf', @response.content_type
224 assert_equal 'application/pdf', @response.content_type
225 end
225 end
226
226
227 def test_index_pdf_with_query_grouped_by_list_custom_field
227 def test_index_pdf_with_query_grouped_by_list_custom_field
228 get :index, :project_id => 1, :query_id => 9, :format => 'pdf'
228 get :index, :project_id => 1, :query_id => 9, :format => 'pdf'
229 assert_response :success
229 assert_response :success
230 assert_not_nil assigns(:issues)
230 assert_not_nil assigns(:issues)
231 assert_not_nil assigns(:issue_count_by_group)
231 assert_not_nil assigns(:issue_count_by_group)
232 assert_equal 'application/pdf', @response.content_type
232 assert_equal 'application/pdf', @response.content_type
233 end
233 end
234
234
235 def test_index_sort
235 def test_index_sort
236 get :index, :sort => 'tracker,id:desc'
236 get :index, :sort => 'tracker,id:desc'
237 assert_response :success
237 assert_response :success
238
238
239 sort_params = @request.session['issues_index_sort']
239 sort_params = @request.session['issues_index_sort']
240 assert sort_params.is_a?(String)
240 assert sort_params.is_a?(String)
241 assert_equal 'tracker,id:desc', sort_params
241 assert_equal 'tracker,id:desc', sort_params
242
242
243 issues = assigns(:issues)
243 issues = assigns(:issues)
244 assert_not_nil issues
244 assert_not_nil issues
245 assert !issues.empty?
245 assert !issues.empty?
246 assert_equal issues.sort {|a,b| a.tracker == b.tracker ? b.id <=> a.id : a.tracker <=> b.tracker }.collect(&:id), issues.collect(&:id)
246 assert_equal issues.sort {|a,b| a.tracker == b.tracker ? b.id <=> a.id : a.tracker <=> b.tracker }.collect(&:id), issues.collect(&:id)
247 end
247 end
248
248
249 def test_index_with_columns
249 def test_index_with_columns
250 columns = ['tracker', 'subject', 'assigned_to']
250 columns = ['tracker', 'subject', 'assigned_to']
251 get :index, :set_filter => 1, :query => { 'column_names' => columns}
251 get :index, :set_filter => 1, :query => { 'column_names' => columns}
252 assert_response :success
252 assert_response :success
253
253
254 # query should use specified columns
254 # query should use specified columns
255 query = assigns(:query)
255 query = assigns(:query)
256 assert_kind_of Query, query
256 assert_kind_of Query, query
257 assert_equal columns, query.column_names.map(&:to_s)
257 assert_equal columns, query.column_names.map(&:to_s)
258
258
259 # columns should be stored in session
259 # columns should be stored in session
260 assert_kind_of Hash, session[:query]
260 assert_kind_of Hash, session[:query]
261 assert_kind_of Array, session[:query][:column_names]
261 assert_kind_of Array, session[:query][:column_names]
262 assert_equal columns, session[:query][:column_names].map(&:to_s)
262 assert_equal columns, session[:query][:column_names].map(&:to_s)
263 end
263 end
264
264
265 def test_show_by_anonymous
265 def test_show_by_anonymous
266 get :show, :id => 1
266 get :show, :id => 1
267 assert_response :success
267 assert_response :success
268 assert_template 'show.rhtml'
268 assert_template 'show.rhtml'
269 assert_not_nil assigns(:issue)
269 assert_not_nil assigns(:issue)
270 assert_equal Issue.find(1), assigns(:issue)
270 assert_equal Issue.find(1), assigns(:issue)
271
271
272 # anonymous role is allowed to add a note
272 # anonymous role is allowed to add a note
273 assert_tag :tag => 'form',
273 assert_tag :tag => 'form',
274 :descendant => { :tag => 'fieldset',
274 :descendant => { :tag => 'fieldset',
275 :child => { :tag => 'legend',
275 :child => { :tag => 'legend',
276 :content => /Notes/ } }
276 :content => /Notes/ } }
277 end
277 end
278
278
279 def test_show_by_manager
279 def test_show_by_manager
280 @request.session[:user_id] = 2
280 @request.session[:user_id] = 2
281 get :show, :id => 1
281 get :show, :id => 1
282 assert_response :success
282 assert_response :success
283
283
284 assert_tag :tag => 'form',
284 assert_tag :tag => 'form',
285 :descendant => { :tag => 'fieldset',
285 :descendant => { :tag => 'fieldset',
286 :child => { :tag => 'legend',
286 :child => { :tag => 'legend',
287 :content => /Change properties/ } },
287 :content => /Change properties/ } },
288 :descendant => { :tag => 'fieldset',
288 :descendant => { :tag => 'fieldset',
289 :child => { :tag => 'legend',
289 :child => { :tag => 'legend',
290 :content => /Log time/ } },
290 :content => /Log time/ } },
291 :descendant => { :tag => 'fieldset',
291 :descendant => { :tag => 'fieldset',
292 :child => { :tag => 'legend',
292 :child => { :tag => 'legend',
293 :content => /Notes/ } }
293 :content => /Notes/ } }
294 end
294 end
295
295
296 def test_show_should_deny_anonymous_access_without_permission
296 def test_show_should_deny_anonymous_access_without_permission
297 Role.anonymous.remove_permission!(:view_issues)
297 Role.anonymous.remove_permission!(:view_issues)
298 get :show, :id => 1
298 get :show, :id => 1
299 assert_response :redirect
299 assert_response :redirect
300 end
300 end
301
301
302 def test_show_should_deny_non_member_access_without_permission
302 def test_show_should_deny_non_member_access_without_permission
303 Role.non_member.remove_permission!(:view_issues)
303 Role.non_member.remove_permission!(:view_issues)
304 @request.session[:user_id] = 9
304 @request.session[:user_id] = 9
305 get :show, :id => 1
305 get :show, :id => 1
306 assert_response 403
306 assert_response 403
307 end
307 end
308
308
309 def test_show_should_deny_member_access_without_permission
309 def test_show_should_deny_member_access_without_permission
310 Role.find(1).remove_permission!(:view_issues)
310 Role.find(1).remove_permission!(:view_issues)
311 @request.session[:user_id] = 2
311 @request.session[:user_id] = 2
312 get :show, :id => 1
312 get :show, :id => 1
313 assert_response 403
313 assert_response 403
314 end
314 end
315
315
316 def test_show_should_not_disclose_relations_to_invisible_issues
316 def test_show_should_not_disclose_relations_to_invisible_issues
317 Setting.cross_project_issue_relations = '1'
317 Setting.cross_project_issue_relations = '1'
318 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(2), :relation_type => 'relates')
318 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(2), :relation_type => 'relates')
319 # Relation to a private project issue
319 # Relation to a private project issue
320 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(4), :relation_type => 'relates')
320 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(4), :relation_type => 'relates')
321
321
322 get :show, :id => 1
322 get :show, :id => 1
323 assert_response :success
323 assert_response :success
324
324
325 assert_tag :div, :attributes => { :id => 'relations' },
325 assert_tag :div, :attributes => { :id => 'relations' },
326 :descendant => { :tag => 'a', :content => /#2$/ }
326 :descendant => { :tag => 'a', :content => /#2$/ }
327 assert_no_tag :div, :attributes => { :id => 'relations' },
327 assert_no_tag :div, :attributes => { :id => 'relations' },
328 :descendant => { :tag => 'a', :content => /#4$/ }
328 :descendant => { :tag => 'a', :content => /#4$/ }
329 end
329 end
330
330
331 def test_show_atom
331 def test_show_atom
332 get :show, :id => 2, :format => 'atom'
332 get :show, :id => 2, :format => 'atom'
333 assert_response :success
333 assert_response :success
334 assert_template 'journals/index.rxml'
334 assert_template 'journals/index.rxml'
335 # Inline image
335 # Inline image
336 assert_select 'content', :text => Regexp.new(Regexp.quote('http://test.host/attachments/download/10'))
336 assert_select 'content', :text => Regexp.new(Regexp.quote('http://test.host/attachments/download/10'))
337 end
337 end
338
338
339 def test_show_export_to_pdf
339 def test_show_export_to_pdf
340 get :show, :id => 3, :format => 'pdf'
340 get :show, :id => 3, :format => 'pdf'
341 assert_response :success
341 assert_response :success
342 assert_equal 'application/pdf', @response.content_type
342 assert_equal 'application/pdf', @response.content_type
343 assert @response.body.starts_with?('%PDF')
343 assert @response.body.starts_with?('%PDF')
344 assert_not_nil assigns(:issue)
344 assert_not_nil assigns(:issue)
345 end
345 end
346
346
347 def test_get_new
347 def test_get_new
348 @request.session[:user_id] = 2
348 @request.session[:user_id] = 2
349 get :new, :project_id => 1, :tracker_id => 1
349 get :new, :project_id => 1, :tracker_id => 1
350 assert_response :success
350 assert_response :success
351 assert_template 'new'
351 assert_template 'new'
352
352
353 assert_tag :tag => 'input', :attributes => { :name => 'issue[custom_field_values][2]',
353 assert_tag :tag => 'input', :attributes => { :name => 'issue[custom_field_values][2]',
354 :value => 'Default string' }
354 :value => 'Default string' }
355 end
355 end
356
356
357 def test_get_new_without_tracker_id
357 def test_get_new_without_tracker_id
358 @request.session[:user_id] = 2
358 @request.session[:user_id] = 2
359 get :new, :project_id => 1
359 get :new, :project_id => 1
360 assert_response :success
360 assert_response :success
361 assert_template 'new'
361 assert_template 'new'
362
362
363 issue = assigns(:issue)
363 issue = assigns(:issue)
364 assert_not_nil issue
364 assert_not_nil issue
365 assert_equal Project.find(1).trackers.first, issue.tracker
365 assert_equal Project.find(1).trackers.first, issue.tracker
366 end
366 end
367
367
368 def test_get_new_with_no_default_status_should_display_an_error
368 def test_get_new_with_no_default_status_should_display_an_error
369 @request.session[:user_id] = 2
369 @request.session[:user_id] = 2
370 IssueStatus.delete_all
370 IssueStatus.delete_all
371
371
372 get :new, :project_id => 1
372 get :new, :project_id => 1
373 assert_response 500
373 assert_response 500
374 assert_error_tag :content => /No default issue/
374 assert_error_tag :content => /No default issue/
375 end
375 end
376
376
377 def test_get_new_with_no_tracker_should_display_an_error
377 def test_get_new_with_no_tracker_should_display_an_error
378 @request.session[:user_id] = 2
378 @request.session[:user_id] = 2
379 Tracker.delete_all
379 Tracker.delete_all
380
380
381 get :new, :project_id => 1
381 get :new, :project_id => 1
382 assert_response 500
382 assert_response 500
383 assert_error_tag :content => /No tracker/
383 assert_error_tag :content => /No tracker/
384 end
384 end
385
385
386 def test_update_new_form
386 def test_update_new_form
387 @request.session[:user_id] = 2
387 @request.session[:user_id] = 2
388 xhr :post, :new, :project_id => 1,
388 xhr :post, :new, :project_id => 1,
389 :issue => {:tracker_id => 2,
389 :issue => {:tracker_id => 2,
390 :subject => 'This is the test_new issue',
390 :subject => 'This is the test_new issue',
391 :description => 'This is the description',
391 :description => 'This is the description',
392 :priority_id => 5}
392 :priority_id => 5}
393 assert_response :success
393 assert_response :success
394 assert_template 'attributes'
394 assert_template 'attributes'
395
395
396 issue = assigns(:issue)
396 issue = assigns(:issue)
397 assert_kind_of Issue, issue
397 assert_kind_of Issue, issue
398 assert_equal 1, issue.project_id
398 assert_equal 1, issue.project_id
399 assert_equal 2, issue.tracker_id
399 assert_equal 2, issue.tracker_id
400 assert_equal 'This is the test_new issue', issue.subject
400 assert_equal 'This is the test_new issue', issue.subject
401 end
401 end
402
402
403 def test_post_create
403 def test_post_create
404 @request.session[:user_id] = 2
404 @request.session[:user_id] = 2
405 assert_difference 'Issue.count' do
405 assert_difference 'Issue.count' do
406 post :create, :project_id => 1,
406 post :create, :project_id => 1,
407 :issue => {:tracker_id => 3,
407 :issue => {:tracker_id => 3,
408 :status_id => 2,
408 :status_id => 2,
409 :subject => 'This is the test_new issue',
409 :subject => 'This is the test_new issue',
410 :description => 'This is the description',
410 :description => 'This is the description',
411 :priority_id => 5,
411 :priority_id => 5,
412 :start_date => '2010-11-07',
412 :start_date => '2010-11-07',
413 :estimated_hours => '',
413 :estimated_hours => '',
414 :custom_field_values => {'2' => 'Value for field 2'}}
414 :custom_field_values => {'2' => 'Value for field 2'}}
415 end
415 end
416 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
416 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
417
417
418 issue = Issue.find_by_subject('This is the test_new issue')
418 issue = Issue.find_by_subject('This is the test_new issue')
419 assert_not_nil issue
419 assert_not_nil issue
420 assert_equal 2, issue.author_id
420 assert_equal 2, issue.author_id
421 assert_equal 3, issue.tracker_id
421 assert_equal 3, issue.tracker_id
422 assert_equal 2, issue.status_id
422 assert_equal 2, issue.status_id
423 assert_equal Date.parse('2010-11-07'), issue.start_date
423 assert_equal Date.parse('2010-11-07'), issue.start_date
424 assert_nil issue.estimated_hours
424 assert_nil issue.estimated_hours
425 v = issue.custom_values.find(:first, :conditions => {:custom_field_id => 2})
425 v = issue.custom_values.find(:first, :conditions => {:custom_field_id => 2})
426 assert_not_nil v
426 assert_not_nil v
427 assert_equal 'Value for field 2', v.value
427 assert_equal 'Value for field 2', v.value
428 end
428 end
429
429
430 def test_post_create_without_start_date
430 def test_post_create_without_start_date
431 @request.session[:user_id] = 2
431 @request.session[:user_id] = 2
432 assert_difference 'Issue.count' do
432 assert_difference 'Issue.count' do
433 post :create, :project_id => 1,
433 post :create, :project_id => 1,
434 :issue => {:tracker_id => 3,
434 :issue => {:tracker_id => 3,
435 :status_id => 2,
435 :status_id => 2,
436 :subject => 'This is the test_new issue',
436 :subject => 'This is the test_new issue',
437 :description => 'This is the description',
437 :description => 'This is the description',
438 :priority_id => 5,
438 :priority_id => 5,
439 :start_date => '',
439 :start_date => '',
440 :estimated_hours => '',
440 :estimated_hours => '',
441 :custom_field_values => {'2' => 'Value for field 2'}}
441 :custom_field_values => {'2' => 'Value for field 2'}}
442 end
442 end
443 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
443 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
444
444
445 issue = Issue.find_by_subject('This is the test_new issue')
445 issue = Issue.find_by_subject('This is the test_new issue')
446 assert_not_nil issue
446 assert_not_nil issue
447 assert_nil issue.start_date
447 assert_nil issue.start_date
448 end
448 end
449
449
450 def test_post_create_and_continue
450 def test_post_create_and_continue
451 @request.session[:user_id] = 2
451 @request.session[:user_id] = 2
452 post :create, :project_id => 1,
452 post :create, :project_id => 1,
453 :issue => {:tracker_id => 3,
453 :issue => {:tracker_id => 3,
454 :subject => 'This is first issue',
454 :subject => 'This is first issue',
455 :priority_id => 5},
455 :priority_id => 5},
456 :continue => ''
456 :continue => ''
457 assert_redirected_to :controller => 'issues', :action => 'new', :project_id => 'ecookbook',
457 assert_redirected_to :controller => 'issues', :action => 'new', :project_id => 'ecookbook',
458 :issue => {:tracker_id => 3}
458 :issue => {:tracker_id => 3}
459 end
459 end
460
460
461 def test_post_create_without_custom_fields_param
461 def test_post_create_without_custom_fields_param
462 @request.session[:user_id] = 2
462 @request.session[:user_id] = 2
463 assert_difference 'Issue.count' do
463 assert_difference 'Issue.count' do
464 post :create, :project_id => 1,
464 post :create, :project_id => 1,
465 :issue => {:tracker_id => 1,
465 :issue => {:tracker_id => 1,
466 :subject => 'This is the test_new issue',
466 :subject => 'This is the test_new issue',
467 :description => 'This is the description',
467 :description => 'This is the description',
468 :priority_id => 5}
468 :priority_id => 5}
469 end
469 end
470 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
470 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
471 end
471 end
472
472
473 def test_post_create_with_required_custom_field_and_without_custom_fields_param
473 def test_post_create_with_required_custom_field_and_without_custom_fields_param
474 field = IssueCustomField.find_by_name('Database')
474 field = IssueCustomField.find_by_name('Database')
475 field.update_attribute(:is_required, true)
475 field.update_attribute(:is_required, true)
476
476
477 @request.session[:user_id] = 2
477 @request.session[:user_id] = 2
478 post :create, :project_id => 1,
478 post :create, :project_id => 1,
479 :issue => {:tracker_id => 1,
479 :issue => {:tracker_id => 1,
480 :subject => 'This is the test_new issue',
480 :subject => 'This is the test_new issue',
481 :description => 'This is the description',
481 :description => 'This is the description',
482 :priority_id => 5}
482 :priority_id => 5}
483 assert_response :success
483 assert_response :success
484 assert_template 'new'
484 assert_template 'new'
485 issue = assigns(:issue)
485 issue = assigns(:issue)
486 assert_not_nil issue
486 assert_not_nil issue
487 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
487 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
488 end
488 end
489
489
490 def test_post_create_with_watchers
490 def test_post_create_with_watchers
491 @request.session[:user_id] = 2
491 @request.session[:user_id] = 2
492 ActionMailer::Base.deliveries.clear
492 ActionMailer::Base.deliveries.clear
493
493
494 assert_difference 'Watcher.count', 2 do
494 assert_difference 'Watcher.count', 2 do
495 post :create, :project_id => 1,
495 post :create, :project_id => 1,
496 :issue => {:tracker_id => 1,
496 :issue => {:tracker_id => 1,
497 :subject => 'This is a new issue with watchers',
497 :subject => 'This is a new issue with watchers',
498 :description => 'This is the description',
498 :description => 'This is the description',
499 :priority_id => 5,
499 :priority_id => 5,
500 :watcher_user_ids => ['2', '3']}
500 :watcher_user_ids => ['2', '3']}
501 end
501 end
502 issue = Issue.find_by_subject('This is a new issue with watchers')
502 issue = Issue.find_by_subject('This is a new issue with watchers')
503 assert_not_nil issue
503 assert_not_nil issue
504 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
504 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
505
505
506 # Watchers added
506 # Watchers added
507 assert_equal [2, 3], issue.watcher_user_ids.sort
507 assert_equal [2, 3], issue.watcher_user_ids.sort
508 assert issue.watched_by?(User.find(3))
508 assert issue.watched_by?(User.find(3))
509 # Watchers notified
509 # Watchers notified
510 mail = ActionMailer::Base.deliveries.last
510 mail = ActionMailer::Base.deliveries.last
511 assert_kind_of TMail::Mail, mail
511 assert_kind_of TMail::Mail, mail
512 assert [mail.bcc, mail.cc].flatten.include?(User.find(3).mail)
512 assert [mail.bcc, mail.cc].flatten.include?(User.find(3).mail)
513 end
513 end
514
514
515 def test_post_create_subissue
515 def test_post_create_subissue
516 @request.session[:user_id] = 2
516 @request.session[:user_id] = 2
517
517
518 assert_difference 'Issue.count' do
518 assert_difference 'Issue.count' do
519 post :create, :project_id => 1,
519 post :create, :project_id => 1,
520 :issue => {:tracker_id => 1,
520 :issue => {:tracker_id => 1,
521 :subject => 'This is a child issue',
521 :subject => 'This is a child issue',
522 :parent_issue_id => 2}
522 :parent_issue_id => 2}
523 end
523 end
524 issue = Issue.find_by_subject('This is a child issue')
524 issue = Issue.find_by_subject('This is a child issue')
525 assert_not_nil issue
525 assert_not_nil issue
526 assert_equal Issue.find(2), issue.parent
526 assert_equal Issue.find(2), issue.parent
527 end
527 end
528
528
529 def test_post_create_subissue_with_non_numeric_parent_id
530 @request.session[:user_id] = 2
531
532 assert_difference 'Issue.count' do
533 post :create, :project_id => 1,
534 :issue => {:tracker_id => 1,
535 :subject => 'This is a child issue',
536 :parent_issue_id => 'ABC'}
537 end
538 issue = Issue.find_by_subject('This is a child issue')
539 assert_not_nil issue
540 assert_nil issue.parent
541 end
542
529 def test_post_create_should_send_a_notification
543 def test_post_create_should_send_a_notification
530 ActionMailer::Base.deliveries.clear
544 ActionMailer::Base.deliveries.clear
531 @request.session[:user_id] = 2
545 @request.session[:user_id] = 2
532 assert_difference 'Issue.count' do
546 assert_difference 'Issue.count' do
533 post :create, :project_id => 1,
547 post :create, :project_id => 1,
534 :issue => {:tracker_id => 3,
548 :issue => {:tracker_id => 3,
535 :subject => 'This is the test_new issue',
549 :subject => 'This is the test_new issue',
536 :description => 'This is the description',
550 :description => 'This is the description',
537 :priority_id => 5,
551 :priority_id => 5,
538 :estimated_hours => '',
552 :estimated_hours => '',
539 :custom_field_values => {'2' => 'Value for field 2'}}
553 :custom_field_values => {'2' => 'Value for field 2'}}
540 end
554 end
541 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
555 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
542
556
543 assert_equal 1, ActionMailer::Base.deliveries.size
557 assert_equal 1, ActionMailer::Base.deliveries.size
544 end
558 end
545
559
546 def test_post_create_should_preserve_fields_values_on_validation_failure
560 def test_post_create_should_preserve_fields_values_on_validation_failure
547 @request.session[:user_id] = 2
561 @request.session[:user_id] = 2
548 post :create, :project_id => 1,
562 post :create, :project_id => 1,
549 :issue => {:tracker_id => 1,
563 :issue => {:tracker_id => 1,
550 # empty subject
564 # empty subject
551 :subject => '',
565 :subject => '',
552 :description => 'This is a description',
566 :description => 'This is a description',
553 :priority_id => 6,
567 :priority_id => 6,
554 :custom_field_values => {'1' => 'Oracle', '2' => 'Value for field 2'}}
568 :custom_field_values => {'1' => 'Oracle', '2' => 'Value for field 2'}}
555 assert_response :success
569 assert_response :success
556 assert_template 'new'
570 assert_template 'new'
557
571
558 assert_tag :textarea, :attributes => { :name => 'issue[description]' },
572 assert_tag :textarea, :attributes => { :name => 'issue[description]' },
559 :content => 'This is a description'
573 :content => 'This is a description'
560 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
574 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
561 :child => { :tag => 'option', :attributes => { :selected => 'selected',
575 :child => { :tag => 'option', :attributes => { :selected => 'selected',
562 :value => '6' },
576 :value => '6' },
563 :content => 'High' }
577 :content => 'High' }
564 # Custom fields
578 # Custom fields
565 assert_tag :select, :attributes => { :name => 'issue[custom_field_values][1]' },
579 assert_tag :select, :attributes => { :name => 'issue[custom_field_values][1]' },
566 :child => { :tag => 'option', :attributes => { :selected => 'selected',
580 :child => { :tag => 'option', :attributes => { :selected => 'selected',
567 :value => 'Oracle' },
581 :value => 'Oracle' },
568 :content => 'Oracle' }
582 :content => 'Oracle' }
569 assert_tag :input, :attributes => { :name => 'issue[custom_field_values][2]',
583 assert_tag :input, :attributes => { :name => 'issue[custom_field_values][2]',
570 :value => 'Value for field 2'}
584 :value => 'Value for field 2'}
571 end
585 end
572
586
573 def test_post_create_should_ignore_non_safe_attributes
587 def test_post_create_should_ignore_non_safe_attributes
574 @request.session[:user_id] = 2
588 @request.session[:user_id] = 2
575 assert_nothing_raised do
589 assert_nothing_raised do
576 post :create, :project_id => 1, :issue => { :tracker => "A param can not be a Tracker" }
590 post :create, :project_id => 1, :issue => { :tracker => "A param can not be a Tracker" }
577 end
591 end
578 end
592 end
579
593
580 context "without workflow privilege" do
594 context "without workflow privilege" do
581 setup do
595 setup do
582 Workflow.delete_all(["role_id = ?", Role.anonymous.id])
596 Workflow.delete_all(["role_id = ?", Role.anonymous.id])
583 Role.anonymous.add_permission! :add_issues, :add_issue_notes
597 Role.anonymous.add_permission! :add_issues, :add_issue_notes
584 end
598 end
585
599
586 context "#new" do
600 context "#new" do
587 should "propose default status only" do
601 should "propose default status only" do
588 get :new, :project_id => 1
602 get :new, :project_id => 1
589 assert_response :success
603 assert_response :success
590 assert_template 'new'
604 assert_template 'new'
591 assert_tag :tag => 'select',
605 assert_tag :tag => 'select',
592 :attributes => {:name => 'issue[status_id]'},
606 :attributes => {:name => 'issue[status_id]'},
593 :children => {:count => 1},
607 :children => {:count => 1},
594 :child => {:tag => 'option', :attributes => {:value => IssueStatus.default.id.to_s}}
608 :child => {:tag => 'option', :attributes => {:value => IssueStatus.default.id.to_s}}
595 end
609 end
596
610
597 should "accept default status" do
611 should "accept default status" do
598 assert_difference 'Issue.count' do
612 assert_difference 'Issue.count' do
599 post :create, :project_id => 1,
613 post :create, :project_id => 1,
600 :issue => {:tracker_id => 1,
614 :issue => {:tracker_id => 1,
601 :subject => 'This is an issue',
615 :subject => 'This is an issue',
602 :status_id => 1}
616 :status_id => 1}
603 end
617 end
604 issue = Issue.last(:order => 'id')
618 issue = Issue.last(:order => 'id')
605 assert_equal IssueStatus.default, issue.status
619 assert_equal IssueStatus.default, issue.status
606 end
620 end
607
621
608 should "accept default status" do
622 should "accept default status" do
609 assert_difference 'Issue.count' do
623 assert_difference 'Issue.count' do
610 post :create, :project_id => 1,
624 post :create, :project_id => 1,
611 :issue => {:tracker_id => 1,
625 :issue => {:tracker_id => 1,
612 :subject => 'This is an issue',
626 :subject => 'This is an issue',
613 :status_id => 1}
627 :status_id => 1}
614 end
628 end
615 issue = Issue.last(:order => 'id')
629 issue = Issue.last(:order => 'id')
616 assert_equal IssueStatus.default, issue.status
630 assert_equal IssueStatus.default, issue.status
617 end
631 end
618
632
619 should "ignore unauthorized status" do
633 should "ignore unauthorized status" do
620 assert_difference 'Issue.count' do
634 assert_difference 'Issue.count' do
621 post :create, :project_id => 1,
635 post :create, :project_id => 1,
622 :issue => {:tracker_id => 1,
636 :issue => {:tracker_id => 1,
623 :subject => 'This is an issue',
637 :subject => 'This is an issue',
624 :status_id => 3}
638 :status_id => 3}
625 end
639 end
626 issue = Issue.last(:order => 'id')
640 issue = Issue.last(:order => 'id')
627 assert_equal IssueStatus.default, issue.status
641 assert_equal IssueStatus.default, issue.status
628 end
642 end
629 end
643 end
630
644
631 context "#update" do
645 context "#update" do
632 should "ignore status change" do
646 should "ignore status change" do
633 assert_difference 'Journal.count' do
647 assert_difference 'Journal.count' do
634 put :update, :id => 1, :notes => 'just trying', :issue => {:status_id => 3}
648 put :update, :id => 1, :notes => 'just trying', :issue => {:status_id => 3}
635 end
649 end
636 assert_equal 1, Issue.find(1).status_id
650 assert_equal 1, Issue.find(1).status_id
637 end
651 end
638
652
639 should "ignore attributes changes" do
653 should "ignore attributes changes" do
640 assert_difference 'Journal.count' do
654 assert_difference 'Journal.count' do
641 put :update, :id => 1, :notes => 'just trying', :issue => {:subject => 'changed', :assigned_to_id => 2}
655 put :update, :id => 1, :notes => 'just trying', :issue => {:subject => 'changed', :assigned_to_id => 2}
642 end
656 end
643 issue = Issue.find(1)
657 issue = Issue.find(1)
644 assert_equal "Can't print recipes", issue.subject
658 assert_equal "Can't print recipes", issue.subject
645 assert_nil issue.assigned_to
659 assert_nil issue.assigned_to
646 end
660 end
647 end
661 end
648 end
662 end
649
663
650 context "with workflow privilege" do
664 context "with workflow privilege" do
651 setup do
665 setup do
652 Workflow.delete_all(["role_id = ?", Role.anonymous.id])
666 Workflow.delete_all(["role_id = ?", Role.anonymous.id])
653 Workflow.create!(:role => Role.anonymous, :tracker_id => 1, :old_status_id => 1, :new_status_id => 3)
667 Workflow.create!(:role => Role.anonymous, :tracker_id => 1, :old_status_id => 1, :new_status_id => 3)
654 Workflow.create!(:role => Role.anonymous, :tracker_id => 1, :old_status_id => 1, :new_status_id => 4)
668 Workflow.create!(:role => Role.anonymous, :tracker_id => 1, :old_status_id => 1, :new_status_id => 4)
655 Role.anonymous.add_permission! :add_issues, :add_issue_notes
669 Role.anonymous.add_permission! :add_issues, :add_issue_notes
656 end
670 end
657
671
658 context "#update" do
672 context "#update" do
659 should "accept authorized status" do
673 should "accept authorized status" do
660 assert_difference 'Journal.count' do
674 assert_difference 'Journal.count' do
661 put :update, :id => 1, :notes => 'just trying', :issue => {:status_id => 3}
675 put :update, :id => 1, :notes => 'just trying', :issue => {:status_id => 3}
662 end
676 end
663 assert_equal 3, Issue.find(1).status_id
677 assert_equal 3, Issue.find(1).status_id
664 end
678 end
665
679
666 should "ignore unauthorized status" do
680 should "ignore unauthorized status" do
667 assert_difference 'Journal.count' do
681 assert_difference 'Journal.count' do
668 put :update, :id => 1, :notes => 'just trying', :issue => {:status_id => 2}
682 put :update, :id => 1, :notes => 'just trying', :issue => {:status_id => 2}
669 end
683 end
670 assert_equal 1, Issue.find(1).status_id
684 assert_equal 1, Issue.find(1).status_id
671 end
685 end
672
686
673 should "accept authorized attributes changes" do
687 should "accept authorized attributes changes" do
674 assert_difference 'Journal.count' do
688 assert_difference 'Journal.count' do
675 put :update, :id => 1, :notes => 'just trying', :issue => {:assigned_to_id => 2}
689 put :update, :id => 1, :notes => 'just trying', :issue => {:assigned_to_id => 2}
676 end
690 end
677 issue = Issue.find(1)
691 issue = Issue.find(1)
678 assert_equal 2, issue.assigned_to_id
692 assert_equal 2, issue.assigned_to_id
679 end
693 end
680
694
681 should "ignore unauthorized attributes changes" do
695 should "ignore unauthorized attributes changes" do
682 assert_difference 'Journal.count' do
696 assert_difference 'Journal.count' do
683 put :update, :id => 1, :notes => 'just trying', :issue => {:subject => 'changed'}
697 put :update, :id => 1, :notes => 'just trying', :issue => {:subject => 'changed'}
684 end
698 end
685 issue = Issue.find(1)
699 issue = Issue.find(1)
686 assert_equal "Can't print recipes", issue.subject
700 assert_equal "Can't print recipes", issue.subject
687 end
701 end
688 end
702 end
689
703
690 context "and :edit_issues permission" do
704 context "and :edit_issues permission" do
691 setup do
705 setup do
692 Role.anonymous.add_permission! :add_issues, :edit_issues
706 Role.anonymous.add_permission! :add_issues, :edit_issues
693 end
707 end
694
708
695 should "accept authorized status" do
709 should "accept authorized status" do
696 assert_difference 'Journal.count' do
710 assert_difference 'Journal.count' do
697 put :update, :id => 1, :notes => 'just trying', :issue => {:status_id => 3}
711 put :update, :id => 1, :notes => 'just trying', :issue => {:status_id => 3}
698 end
712 end
699 assert_equal 3, Issue.find(1).status_id
713 assert_equal 3, Issue.find(1).status_id
700 end
714 end
701
715
702 should "ignore unauthorized status" do
716 should "ignore unauthorized status" do
703 assert_difference 'Journal.count' do
717 assert_difference 'Journal.count' do
704 put :update, :id => 1, :notes => 'just trying', :issue => {:status_id => 2}
718 put :update, :id => 1, :notes => 'just trying', :issue => {:status_id => 2}
705 end
719 end
706 assert_equal 1, Issue.find(1).status_id
720 assert_equal 1, Issue.find(1).status_id
707 end
721 end
708
722
709 should "accept authorized attributes changes" do
723 should "accept authorized attributes changes" do
710 assert_difference 'Journal.count' do
724 assert_difference 'Journal.count' do
711 put :update, :id => 1, :notes => 'just trying', :issue => {:subject => 'changed', :assigned_to_id => 2}
725 put :update, :id => 1, :notes => 'just trying', :issue => {:subject => 'changed', :assigned_to_id => 2}
712 end
726 end
713 issue = Issue.find(1)
727 issue = Issue.find(1)
714 assert_equal "changed", issue.subject
728 assert_equal "changed", issue.subject
715 assert_equal 2, issue.assigned_to_id
729 assert_equal 2, issue.assigned_to_id
716 end
730 end
717 end
731 end
718 end
732 end
719
733
720 def test_copy_issue
734 def test_copy_issue
721 @request.session[:user_id] = 2
735 @request.session[:user_id] = 2
722 get :new, :project_id => 1, :copy_from => 1
736 get :new, :project_id => 1, :copy_from => 1
723 assert_template 'new'
737 assert_template 'new'
724 assert_not_nil assigns(:issue)
738 assert_not_nil assigns(:issue)
725 orig = Issue.find(1)
739 orig = Issue.find(1)
726 assert_equal orig.subject, assigns(:issue).subject
740 assert_equal orig.subject, assigns(:issue).subject
727 end
741 end
728
742
729 def test_get_edit
743 def test_get_edit
730 @request.session[:user_id] = 2
744 @request.session[:user_id] = 2
731 get :edit, :id => 1
745 get :edit, :id => 1
732 assert_response :success
746 assert_response :success
733 assert_template 'edit'
747 assert_template 'edit'
734 assert_not_nil assigns(:issue)
748 assert_not_nil assigns(:issue)
735 assert_equal Issue.find(1), assigns(:issue)
749 assert_equal Issue.find(1), assigns(:issue)
736 end
750 end
737
751
738 def test_get_edit_with_params
752 def test_get_edit_with_params
739 @request.session[:user_id] = 2
753 @request.session[:user_id] = 2
740 get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 },
754 get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 },
741 :time_entry => { :hours => '2.5', :comments => 'test_get_edit_with_params', :activity_id => TimeEntryActivity.first.id }
755 :time_entry => { :hours => '2.5', :comments => 'test_get_edit_with_params', :activity_id => TimeEntryActivity.first.id }
742 assert_response :success
756 assert_response :success
743 assert_template 'edit'
757 assert_template 'edit'
744
758
745 issue = assigns(:issue)
759 issue = assigns(:issue)
746 assert_not_nil issue
760 assert_not_nil issue
747
761
748 assert_equal 5, issue.status_id
762 assert_equal 5, issue.status_id
749 assert_tag :select, :attributes => { :name => 'issue[status_id]' },
763 assert_tag :select, :attributes => { :name => 'issue[status_id]' },
750 :child => { :tag => 'option',
764 :child => { :tag => 'option',
751 :content => 'Closed',
765 :content => 'Closed',
752 :attributes => { :selected => 'selected' } }
766 :attributes => { :selected => 'selected' } }
753
767
754 assert_equal 7, issue.priority_id
768 assert_equal 7, issue.priority_id
755 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
769 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
756 :child => { :tag => 'option',
770 :child => { :tag => 'option',
757 :content => 'Urgent',
771 :content => 'Urgent',
758 :attributes => { :selected => 'selected' } }
772 :attributes => { :selected => 'selected' } }
759
773
760 assert_tag :input, :attributes => { :name => 'time_entry[hours]', :value => '2.5' }
774 assert_tag :input, :attributes => { :name => 'time_entry[hours]', :value => '2.5' }
761 assert_tag :select, :attributes => { :name => 'time_entry[activity_id]' },
775 assert_tag :select, :attributes => { :name => 'time_entry[activity_id]' },
762 :child => { :tag => 'option',
776 :child => { :tag => 'option',
763 :attributes => { :selected => 'selected', :value => TimeEntryActivity.first.id } }
777 :attributes => { :selected => 'selected', :value => TimeEntryActivity.first.id } }
764 assert_tag :input, :attributes => { :name => 'time_entry[comments]', :value => 'test_get_edit_with_params' }
778 assert_tag :input, :attributes => { :name => 'time_entry[comments]', :value => 'test_get_edit_with_params' }
765 end
779 end
766
780
767 def test_update_edit_form
781 def test_update_edit_form
768 @request.session[:user_id] = 2
782 @request.session[:user_id] = 2
769 xhr :post, :new, :project_id => 1,
783 xhr :post, :new, :project_id => 1,
770 :id => 1,
784 :id => 1,
771 :issue => {:tracker_id => 2,
785 :issue => {:tracker_id => 2,
772 :subject => 'This is the test_new issue',
786 :subject => 'This is the test_new issue',
773 :description => 'This is the description',
787 :description => 'This is the description',
774 :priority_id => 5}
788 :priority_id => 5}
775 assert_response :success
789 assert_response :success
776 assert_template 'attributes'
790 assert_template 'attributes'
777
791
778 issue = assigns(:issue)
792 issue = assigns(:issue)
779 assert_kind_of Issue, issue
793 assert_kind_of Issue, issue
780 assert_equal 1, issue.id
794 assert_equal 1, issue.id
781 assert_equal 1, issue.project_id
795 assert_equal 1, issue.project_id
782 assert_equal 2, issue.tracker_id
796 assert_equal 2, issue.tracker_id
783 assert_equal 'This is the test_new issue', issue.subject
797 assert_equal 'This is the test_new issue', issue.subject
784 end
798 end
785
799
786 def test_update_using_invalid_http_verbs
800 def test_update_using_invalid_http_verbs
787 @request.session[:user_id] = 2
801 @request.session[:user_id] = 2
788 subject = 'Updated by an invalid http verb'
802 subject = 'Updated by an invalid http verb'
789
803
790 get :update, :id => 1, :issue => {:subject => subject}
804 get :update, :id => 1, :issue => {:subject => subject}
791 assert_not_equal subject, Issue.find(1).subject
805 assert_not_equal subject, Issue.find(1).subject
792
806
793 post :update, :id => 1, :issue => {:subject => subject}
807 post :update, :id => 1, :issue => {:subject => subject}
794 assert_not_equal subject, Issue.find(1).subject
808 assert_not_equal subject, Issue.find(1).subject
795
809
796 delete :update, :id => 1, :issue => {:subject => subject}
810 delete :update, :id => 1, :issue => {:subject => subject}
797 assert_not_equal subject, Issue.find(1).subject
811 assert_not_equal subject, Issue.find(1).subject
798 end
812 end
799
813
800 def test_put_update_without_custom_fields_param
814 def test_put_update_without_custom_fields_param
801 @request.session[:user_id] = 2
815 @request.session[:user_id] = 2
802 ActionMailer::Base.deliveries.clear
816 ActionMailer::Base.deliveries.clear
803
817
804 issue = Issue.find(1)
818 issue = Issue.find(1)
805 assert_equal '125', issue.custom_value_for(2).value
819 assert_equal '125', issue.custom_value_for(2).value
806 old_subject = issue.subject
820 old_subject = issue.subject
807 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
821 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
808
822
809 assert_difference('Journal.count') do
823 assert_difference('Journal.count') do
810 assert_difference('JournalDetail.count', 2) do
824 assert_difference('JournalDetail.count', 2) do
811 put :update, :id => 1, :issue => {:subject => new_subject,
825 put :update, :id => 1, :issue => {:subject => new_subject,
812 :priority_id => '6',
826 :priority_id => '6',
813 :category_id => '1' # no change
827 :category_id => '1' # no change
814 }
828 }
815 end
829 end
816 end
830 end
817 assert_redirected_to :action => 'show', :id => '1'
831 assert_redirected_to :action => 'show', :id => '1'
818 issue.reload
832 issue.reload
819 assert_equal new_subject, issue.subject
833 assert_equal new_subject, issue.subject
820 # Make sure custom fields were not cleared
834 # Make sure custom fields were not cleared
821 assert_equal '125', issue.custom_value_for(2).value
835 assert_equal '125', issue.custom_value_for(2).value
822
836
823 mail = ActionMailer::Base.deliveries.last
837 mail = ActionMailer::Base.deliveries.last
824 assert_kind_of TMail::Mail, mail
838 assert_kind_of TMail::Mail, mail
825 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
839 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
826 assert mail.body.include?("Subject changed from #{old_subject} to #{new_subject}")
840 assert mail.body.include?("Subject changed from #{old_subject} to #{new_subject}")
827 end
841 end
828
842
829 def test_put_update_with_custom_field_change
843 def test_put_update_with_custom_field_change
830 @request.session[:user_id] = 2
844 @request.session[:user_id] = 2
831 issue = Issue.find(1)
845 issue = Issue.find(1)
832 assert_equal '125', issue.custom_value_for(2).value
846 assert_equal '125', issue.custom_value_for(2).value
833
847
834 assert_difference('Journal.count') do
848 assert_difference('Journal.count') do
835 assert_difference('JournalDetail.count', 3) do
849 assert_difference('JournalDetail.count', 3) do
836 put :update, :id => 1, :issue => {:subject => 'Custom field change',
850 put :update, :id => 1, :issue => {:subject => 'Custom field change',
837 :priority_id => '6',
851 :priority_id => '6',
838 :category_id => '1', # no change
852 :category_id => '1', # no change
839 :custom_field_values => { '2' => 'New custom value' }
853 :custom_field_values => { '2' => 'New custom value' }
840 }
854 }
841 end
855 end
842 end
856 end
843 assert_redirected_to :action => 'show', :id => '1'
857 assert_redirected_to :action => 'show', :id => '1'
844 issue.reload
858 issue.reload
845 assert_equal 'New custom value', issue.custom_value_for(2).value
859 assert_equal 'New custom value', issue.custom_value_for(2).value
846
860
847 mail = ActionMailer::Base.deliveries.last
861 mail = ActionMailer::Base.deliveries.last
848 assert_kind_of TMail::Mail, mail
862 assert_kind_of TMail::Mail, mail
849 assert mail.body.include?("Searchable field changed from 125 to New custom value")
863 assert mail.body.include?("Searchable field changed from 125 to New custom value")
850 end
864 end
851
865
852 def test_put_update_with_status_and_assignee_change
866 def test_put_update_with_status_and_assignee_change
853 issue = Issue.find(1)
867 issue = Issue.find(1)
854 assert_equal 1, issue.status_id
868 assert_equal 1, issue.status_id
855 @request.session[:user_id] = 2
869 @request.session[:user_id] = 2
856 assert_difference('TimeEntry.count', 0) do
870 assert_difference('TimeEntry.count', 0) do
857 put :update,
871 put :update,
858 :id => 1,
872 :id => 1,
859 :issue => { :status_id => 2, :assigned_to_id => 3 },
873 :issue => { :status_id => 2, :assigned_to_id => 3 },
860 :notes => 'Assigned to dlopper',
874 :notes => 'Assigned to dlopper',
861 :time_entry => { :hours => '', :comments => '', :activity_id => TimeEntryActivity.first }
875 :time_entry => { :hours => '', :comments => '', :activity_id => TimeEntryActivity.first }
862 end
876 end
863 assert_redirected_to :action => 'show', :id => '1'
877 assert_redirected_to :action => 'show', :id => '1'
864 issue.reload
878 issue.reload
865 assert_equal 2, issue.status_id
879 assert_equal 2, issue.status_id
866 j = Journal.find(:first, :order => 'id DESC')
880 j = Journal.find(:first, :order => 'id DESC')
867 assert_equal 'Assigned to dlopper', j.notes
881 assert_equal 'Assigned to dlopper', j.notes
868 assert_equal 2, j.details.size
882 assert_equal 2, j.details.size
869
883
870 mail = ActionMailer::Base.deliveries.last
884 mail = ActionMailer::Base.deliveries.last
871 assert mail.body.include?("Status changed from New to Assigned")
885 assert mail.body.include?("Status changed from New to Assigned")
872 # subject should contain the new status
886 # subject should contain the new status
873 assert mail.subject.include?("(#{ IssueStatus.find(2).name })")
887 assert mail.subject.include?("(#{ IssueStatus.find(2).name })")
874 end
888 end
875
889
876 def test_put_update_with_note_only
890 def test_put_update_with_note_only
877 notes = 'Note added by IssuesControllerTest#test_update_with_note_only'
891 notes = 'Note added by IssuesControllerTest#test_update_with_note_only'
878 # anonymous user
892 # anonymous user
879 put :update,
893 put :update,
880 :id => 1,
894 :id => 1,
881 :notes => notes
895 :notes => notes
882 assert_redirected_to :action => 'show', :id => '1'
896 assert_redirected_to :action => 'show', :id => '1'
883 j = Journal.find(:first, :order => 'id DESC')
897 j = Journal.find(:first, :order => 'id DESC')
884 assert_equal notes, j.notes
898 assert_equal notes, j.notes
885 assert_equal 0, j.details.size
899 assert_equal 0, j.details.size
886 assert_equal User.anonymous, j.user
900 assert_equal User.anonymous, j.user
887
901
888 mail = ActionMailer::Base.deliveries.last
902 mail = ActionMailer::Base.deliveries.last
889 assert mail.body.include?(notes)
903 assert mail.body.include?(notes)
890 end
904 end
891
905
892 def test_put_update_with_note_and_spent_time
906 def test_put_update_with_note_and_spent_time
893 @request.session[:user_id] = 2
907 @request.session[:user_id] = 2
894 spent_hours_before = Issue.find(1).spent_hours
908 spent_hours_before = Issue.find(1).spent_hours
895 assert_difference('TimeEntry.count') do
909 assert_difference('TimeEntry.count') do
896 put :update,
910 put :update,
897 :id => 1,
911 :id => 1,
898 :notes => '2.5 hours added',
912 :notes => '2.5 hours added',
899 :time_entry => { :hours => '2.5', :comments => 'test_put_update_with_note_and_spent_time', :activity_id => TimeEntryActivity.first.id }
913 :time_entry => { :hours => '2.5', :comments => 'test_put_update_with_note_and_spent_time', :activity_id => TimeEntryActivity.first.id }
900 end
914 end
901 assert_redirected_to :action => 'show', :id => '1'
915 assert_redirected_to :action => 'show', :id => '1'
902
916
903 issue = Issue.find(1)
917 issue = Issue.find(1)
904
918
905 j = Journal.find(:first, :order => 'id DESC')
919 j = Journal.find(:first, :order => 'id DESC')
906 assert_equal '2.5 hours added', j.notes
920 assert_equal '2.5 hours added', j.notes
907 assert_equal 0, j.details.size
921 assert_equal 0, j.details.size
908
922
909 t = issue.time_entries.find_by_comments('test_put_update_with_note_and_spent_time')
923 t = issue.time_entries.find_by_comments('test_put_update_with_note_and_spent_time')
910 assert_not_nil t
924 assert_not_nil t
911 assert_equal 2.5, t.hours
925 assert_equal 2.5, t.hours
912 assert_equal spent_hours_before + 2.5, issue.spent_hours
926 assert_equal spent_hours_before + 2.5, issue.spent_hours
913 end
927 end
914
928
915 def test_put_update_with_attachment_only
929 def test_put_update_with_attachment_only
916 set_tmp_attachments_directory
930 set_tmp_attachments_directory
917
931
918 # Delete all fixtured journals, a race condition can occur causing the wrong
932 # Delete all fixtured journals, a race condition can occur causing the wrong
919 # journal to get fetched in the next find.
933 # journal to get fetched in the next find.
920 Journal.delete_all
934 Journal.delete_all
921
935
922 # anonymous user
936 # anonymous user
923 put :update,
937 put :update,
924 :id => 1,
938 :id => 1,
925 :notes => '',
939 :notes => '',
926 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
940 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
927 assert_redirected_to :action => 'show', :id => '1'
941 assert_redirected_to :action => 'show', :id => '1'
928 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
942 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
929 assert j.notes.blank?
943 assert j.notes.blank?
930 assert_equal 1, j.details.size
944 assert_equal 1, j.details.size
931 assert_equal 'testfile.txt', j.details.first.value
945 assert_equal 'testfile.txt', j.details.first.value
932 assert_equal User.anonymous, j.user
946 assert_equal User.anonymous, j.user
933
947
934 mail = ActionMailer::Base.deliveries.last
948 mail = ActionMailer::Base.deliveries.last
935 assert mail.body.include?('testfile.txt')
949 assert mail.body.include?('testfile.txt')
936 end
950 end
937
951
938 def test_put_update_with_attachment_that_fails_to_save
952 def test_put_update_with_attachment_that_fails_to_save
939 set_tmp_attachments_directory
953 set_tmp_attachments_directory
940
954
941 # Delete all fixtured journals, a race condition can occur causing the wrong
955 # Delete all fixtured journals, a race condition can occur causing the wrong
942 # journal to get fetched in the next find.
956 # journal to get fetched in the next find.
943 Journal.delete_all
957 Journal.delete_all
944
958
945 # Mock out the unsaved attachment
959 # Mock out the unsaved attachment
946 Attachment.any_instance.stubs(:create).returns(Attachment.new)
960 Attachment.any_instance.stubs(:create).returns(Attachment.new)
947
961
948 # anonymous user
962 # anonymous user
949 put :update,
963 put :update,
950 :id => 1,
964 :id => 1,
951 :notes => '',
965 :notes => '',
952 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
966 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
953 assert_redirected_to :action => 'show', :id => '1'
967 assert_redirected_to :action => 'show', :id => '1'
954 assert_equal '1 file(s) could not be saved.', flash[:warning]
968 assert_equal '1 file(s) could not be saved.', flash[:warning]
955
969
956 end if Object.const_defined?(:Mocha)
970 end if Object.const_defined?(:Mocha)
957
971
958 def test_put_update_with_no_change
972 def test_put_update_with_no_change
959 issue = Issue.find(1)
973 issue = Issue.find(1)
960 issue.journals.clear
974 issue.journals.clear
961 ActionMailer::Base.deliveries.clear
975 ActionMailer::Base.deliveries.clear
962
976
963 put :update,
977 put :update,
964 :id => 1,
978 :id => 1,
965 :notes => ''
979 :notes => ''
966 assert_redirected_to :action => 'show', :id => '1'
980 assert_redirected_to :action => 'show', :id => '1'
967
981
968 issue.reload
982 issue.reload
969 assert issue.journals.empty?
983 assert issue.journals.empty?
970 # No email should be sent
984 # No email should be sent
971 assert ActionMailer::Base.deliveries.empty?
985 assert ActionMailer::Base.deliveries.empty?
972 end
986 end
973
987
974 def test_put_update_should_send_a_notification
988 def test_put_update_should_send_a_notification
975 @request.session[:user_id] = 2
989 @request.session[:user_id] = 2
976 ActionMailer::Base.deliveries.clear
990 ActionMailer::Base.deliveries.clear
977 issue = Issue.find(1)
991 issue = Issue.find(1)
978 old_subject = issue.subject
992 old_subject = issue.subject
979 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
993 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
980
994
981 put :update, :id => 1, :issue => {:subject => new_subject,
995 put :update, :id => 1, :issue => {:subject => new_subject,
982 :priority_id => '6',
996 :priority_id => '6',
983 :category_id => '1' # no change
997 :category_id => '1' # no change
984 }
998 }
985 assert_equal 1, ActionMailer::Base.deliveries.size
999 assert_equal 1, ActionMailer::Base.deliveries.size
986 end
1000 end
987
1001
988 def test_put_update_with_invalid_spent_time
1002 def test_put_update_with_invalid_spent_time
989 @request.session[:user_id] = 2
1003 @request.session[:user_id] = 2
990 notes = 'Note added by IssuesControllerTest#test_post_edit_with_invalid_spent_time'
1004 notes = 'Note added by IssuesControllerTest#test_post_edit_with_invalid_spent_time'
991
1005
992 assert_no_difference('Journal.count') do
1006 assert_no_difference('Journal.count') do
993 put :update,
1007 put :update,
994 :id => 1,
1008 :id => 1,
995 :notes => notes,
1009 :notes => notes,
996 :time_entry => {"comments"=>"", "activity_id"=>"", "hours"=>"2z"}
1010 :time_entry => {"comments"=>"", "activity_id"=>"", "hours"=>"2z"}
997 end
1011 end
998 assert_response :success
1012 assert_response :success
999 assert_template 'edit'
1013 assert_template 'edit'
1000
1014
1001 assert_tag :textarea, :attributes => { :name => 'notes' },
1015 assert_tag :textarea, :attributes => { :name => 'notes' },
1002 :content => notes
1016 :content => notes
1003 assert_tag :input, :attributes => { :name => 'time_entry[hours]', :value => "2z" }
1017 assert_tag :input, :attributes => { :name => 'time_entry[hours]', :value => "2z" }
1004 end
1018 end
1005
1019
1006 def test_put_update_should_allow_fixed_version_to_be_set_to_a_subproject
1020 def test_put_update_should_allow_fixed_version_to_be_set_to_a_subproject
1007 issue = Issue.find(2)
1021 issue = Issue.find(2)
1008 @request.session[:user_id] = 2
1022 @request.session[:user_id] = 2
1009
1023
1010 put :update,
1024 put :update,
1011 :id => issue.id,
1025 :id => issue.id,
1012 :issue => {
1026 :issue => {
1013 :fixed_version_id => 4
1027 :fixed_version_id => 4
1014 }
1028 }
1015
1029
1016 assert_response :redirect
1030 assert_response :redirect
1017 issue.reload
1031 issue.reload
1018 assert_equal 4, issue.fixed_version_id
1032 assert_equal 4, issue.fixed_version_id
1019 assert_not_equal issue.project_id, issue.fixed_version.project_id
1033 assert_not_equal issue.project_id, issue.fixed_version.project_id
1020 end
1034 end
1021
1035
1022 def test_put_update_should_redirect_back_using_the_back_url_parameter
1036 def test_put_update_should_redirect_back_using_the_back_url_parameter
1023 issue = Issue.find(2)
1037 issue = Issue.find(2)
1024 @request.session[:user_id] = 2
1038 @request.session[:user_id] = 2
1025
1039
1026 put :update,
1040 put :update,
1027 :id => issue.id,
1041 :id => issue.id,
1028 :issue => {
1042 :issue => {
1029 :fixed_version_id => 4
1043 :fixed_version_id => 4
1030 },
1044 },
1031 :back_url => '/issues'
1045 :back_url => '/issues'
1032
1046
1033 assert_response :redirect
1047 assert_response :redirect
1034 assert_redirected_to '/issues'
1048 assert_redirected_to '/issues'
1035 end
1049 end
1036
1050
1037 def test_put_update_should_not_redirect_back_using_the_back_url_parameter_off_the_host
1051 def test_put_update_should_not_redirect_back_using_the_back_url_parameter_off_the_host
1038 issue = Issue.find(2)
1052 issue = Issue.find(2)
1039 @request.session[:user_id] = 2
1053 @request.session[:user_id] = 2
1040
1054
1041 put :update,
1055 put :update,
1042 :id => issue.id,
1056 :id => issue.id,
1043 :issue => {
1057 :issue => {
1044 :fixed_version_id => 4
1058 :fixed_version_id => 4
1045 },
1059 },
1046 :back_url => 'http://google.com'
1060 :back_url => 'http://google.com'
1047
1061
1048 assert_response :redirect
1062 assert_response :redirect
1049 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue.id
1063 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue.id
1050 end
1064 end
1051
1065
1052 def test_get_bulk_edit
1066 def test_get_bulk_edit
1053 @request.session[:user_id] = 2
1067 @request.session[:user_id] = 2
1054 get :bulk_edit, :ids => [1, 2]
1068 get :bulk_edit, :ids => [1, 2]
1055 assert_response :success
1069 assert_response :success
1056 assert_template 'bulk_edit'
1070 assert_template 'bulk_edit'
1057
1071
1058 # Project specific custom field, date type
1072 # Project specific custom field, date type
1059 field = CustomField.find(9)
1073 field = CustomField.find(9)
1060 assert !field.is_for_all?
1074 assert !field.is_for_all?
1061 assert_equal 'date', field.field_format
1075 assert_equal 'date', field.field_format
1062 assert_tag :input, :attributes => {:name => 'issue[custom_field_values][9]'}
1076 assert_tag :input, :attributes => {:name => 'issue[custom_field_values][9]'}
1063
1077
1064 # System wide custom field
1078 # System wide custom field
1065 assert CustomField.find(1).is_for_all?
1079 assert CustomField.find(1).is_for_all?
1066 assert_tag :select, :attributes => {:name => 'issue[custom_field_values][1]'}
1080 assert_tag :select, :attributes => {:name => 'issue[custom_field_values][1]'}
1067 end
1081 end
1068
1082
1069 def test_get_bulk_edit_on_different_projects
1083 def test_get_bulk_edit_on_different_projects
1070 @request.session[:user_id] = 2
1084 @request.session[:user_id] = 2
1071 get :bulk_edit, :ids => [1, 2, 6]
1085 get :bulk_edit, :ids => [1, 2, 6]
1072 assert_response :success
1086 assert_response :success
1073 assert_template 'bulk_edit'
1087 assert_template 'bulk_edit'
1074
1088
1075 # Project specific custom field, date type
1089 # Project specific custom field, date type
1076 field = CustomField.find(9)
1090 field = CustomField.find(9)
1077 assert !field.is_for_all?
1091 assert !field.is_for_all?
1078 assert !field.project_ids.include?(Issue.find(6).project_id)
1092 assert !field.project_ids.include?(Issue.find(6).project_id)
1079 assert_no_tag :input, :attributes => {:name => 'issue[custom_field_values][9]'}
1093 assert_no_tag :input, :attributes => {:name => 'issue[custom_field_values][9]'}
1080 end
1094 end
1081
1095
1082 def test_bulk_update
1096 def test_bulk_update
1083 @request.session[:user_id] = 2
1097 @request.session[:user_id] = 2
1084 # update issues priority
1098 # update issues priority
1085 post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing',
1099 post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing',
1086 :issue => {:priority_id => 7,
1100 :issue => {:priority_id => 7,
1087 :assigned_to_id => '',
1101 :assigned_to_id => '',
1088 :custom_field_values => {'2' => ''}}
1102 :custom_field_values => {'2' => ''}}
1089
1103
1090 assert_response 302
1104 assert_response 302
1091 # check that the issues were updated
1105 # check that the issues were updated
1092 assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id}
1106 assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id}
1093
1107
1094 issue = Issue.find(1)
1108 issue = Issue.find(1)
1095 journal = issue.journals.find(:first, :order => 'created_on DESC')
1109 journal = issue.journals.find(:first, :order => 'created_on DESC')
1096 assert_equal '125', issue.custom_value_for(2).value
1110 assert_equal '125', issue.custom_value_for(2).value
1097 assert_equal 'Bulk editing', journal.notes
1111 assert_equal 'Bulk editing', journal.notes
1098 assert_equal 1, journal.details.size
1112 assert_equal 1, journal.details.size
1099 end
1113 end
1100
1114
1101 def test_bulk_update_on_different_projects
1115 def test_bulk_update_on_different_projects
1102 @request.session[:user_id] = 2
1116 @request.session[:user_id] = 2
1103 # update issues priority
1117 # update issues priority
1104 post :bulk_update, :ids => [1, 2, 6], :notes => 'Bulk editing',
1118 post :bulk_update, :ids => [1, 2, 6], :notes => 'Bulk editing',
1105 :issue => {:priority_id => 7,
1119 :issue => {:priority_id => 7,
1106 :assigned_to_id => '',
1120 :assigned_to_id => '',
1107 :custom_field_values => {'2' => ''}}
1121 :custom_field_values => {'2' => ''}}
1108
1122
1109 assert_response 302
1123 assert_response 302
1110 # check that the issues were updated
1124 # check that the issues were updated
1111 assert_equal [7, 7, 7], Issue.find([1,2,6]).map(&:priority_id)
1125 assert_equal [7, 7, 7], Issue.find([1,2,6]).map(&:priority_id)
1112
1126
1113 issue = Issue.find(1)
1127 issue = Issue.find(1)
1114 journal = issue.journals.find(:first, :order => 'created_on DESC')
1128 journal = issue.journals.find(:first, :order => 'created_on DESC')
1115 assert_equal '125', issue.custom_value_for(2).value
1129 assert_equal '125', issue.custom_value_for(2).value
1116 assert_equal 'Bulk editing', journal.notes
1130 assert_equal 'Bulk editing', journal.notes
1117 assert_equal 1, journal.details.size
1131 assert_equal 1, journal.details.size
1118 end
1132 end
1119
1133
1120 def test_bulk_update_on_different_projects_without_rights
1134 def test_bulk_update_on_different_projects_without_rights
1121 @request.session[:user_id] = 3
1135 @request.session[:user_id] = 3
1122 user = User.find(3)
1136 user = User.find(3)
1123 action = { :controller => "issues", :action => "bulk_update" }
1137 action = { :controller => "issues", :action => "bulk_update" }
1124 assert user.allowed_to?(action, Issue.find(1).project)
1138 assert user.allowed_to?(action, Issue.find(1).project)
1125 assert ! user.allowed_to?(action, Issue.find(6).project)
1139 assert ! user.allowed_to?(action, Issue.find(6).project)
1126 post :bulk_update, :ids => [1, 6], :notes => 'Bulk should fail',
1140 post :bulk_update, :ids => [1, 6], :notes => 'Bulk should fail',
1127 :issue => {:priority_id => 7,
1141 :issue => {:priority_id => 7,
1128 :assigned_to_id => '',
1142 :assigned_to_id => '',
1129 :custom_field_values => {'2' => ''}}
1143 :custom_field_values => {'2' => ''}}
1130 assert_response 403
1144 assert_response 403
1131 assert_not_equal "Bulk should fail", Journal.last.notes
1145 assert_not_equal "Bulk should fail", Journal.last.notes
1132 end
1146 end
1133
1147
1134 def test_bullk_update_should_send_a_notification
1148 def test_bullk_update_should_send_a_notification
1135 @request.session[:user_id] = 2
1149 @request.session[:user_id] = 2
1136 ActionMailer::Base.deliveries.clear
1150 ActionMailer::Base.deliveries.clear
1137 post(:bulk_update,
1151 post(:bulk_update,
1138 {
1152 {
1139 :ids => [1, 2],
1153 :ids => [1, 2],
1140 :notes => 'Bulk editing',
1154 :notes => 'Bulk editing',
1141 :issue => {
1155 :issue => {
1142 :priority_id => 7,
1156 :priority_id => 7,
1143 :assigned_to_id => '',
1157 :assigned_to_id => '',
1144 :custom_field_values => {'2' => ''}
1158 :custom_field_values => {'2' => ''}
1145 }
1159 }
1146 })
1160 })
1147
1161
1148 assert_response 302
1162 assert_response 302
1149 assert_equal 2, ActionMailer::Base.deliveries.size
1163 assert_equal 2, ActionMailer::Base.deliveries.size
1150 end
1164 end
1151
1165
1152 def test_bulk_update_status
1166 def test_bulk_update_status
1153 @request.session[:user_id] = 2
1167 @request.session[:user_id] = 2
1154 # update issues priority
1168 # update issues priority
1155 post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing status',
1169 post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing status',
1156 :issue => {:priority_id => '',
1170 :issue => {:priority_id => '',
1157 :assigned_to_id => '',
1171 :assigned_to_id => '',
1158 :status_id => '5'}
1172 :status_id => '5'}
1159
1173
1160 assert_response 302
1174 assert_response 302
1161 issue = Issue.find(1)
1175 issue = Issue.find(1)
1162 assert issue.closed?
1176 assert issue.closed?
1163 end
1177 end
1164
1178
1165 def test_bulk_update_custom_field
1179 def test_bulk_update_custom_field
1166 @request.session[:user_id] = 2
1180 @request.session[:user_id] = 2
1167 # update issues priority
1181 # update issues priority
1168 post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing custom field',
1182 post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing custom field',
1169 :issue => {:priority_id => '',
1183 :issue => {:priority_id => '',
1170 :assigned_to_id => '',
1184 :assigned_to_id => '',
1171 :custom_field_values => {'2' => '777'}}
1185 :custom_field_values => {'2' => '777'}}
1172
1186
1173 assert_response 302
1187 assert_response 302
1174
1188
1175 issue = Issue.find(1)
1189 issue = Issue.find(1)
1176 journal = issue.journals.find(:first, :order => 'created_on DESC')
1190 journal = issue.journals.find(:first, :order => 'created_on DESC')
1177 assert_equal '777', issue.custom_value_for(2).value
1191 assert_equal '777', issue.custom_value_for(2).value
1178 assert_equal 1, journal.details.size
1192 assert_equal 1, journal.details.size
1179 assert_equal '125', journal.details.first.old_value
1193 assert_equal '125', journal.details.first.old_value
1180 assert_equal '777', journal.details.first.value
1194 assert_equal '777', journal.details.first.value
1181 end
1195 end
1182
1196
1183 def test_bulk_update_unassign
1197 def test_bulk_update_unassign
1184 assert_not_nil Issue.find(2).assigned_to
1198 assert_not_nil Issue.find(2).assigned_to
1185 @request.session[:user_id] = 2
1199 @request.session[:user_id] = 2
1186 # unassign issues
1200 # unassign issues
1187 post :bulk_update, :ids => [1, 2], :notes => 'Bulk unassigning', :issue => {:assigned_to_id => 'none'}
1201 post :bulk_update, :ids => [1, 2], :notes => 'Bulk unassigning', :issue => {:assigned_to_id => 'none'}
1188 assert_response 302
1202 assert_response 302
1189 # check that the issues were updated
1203 # check that the issues were updated
1190 assert_nil Issue.find(2).assigned_to
1204 assert_nil Issue.find(2).assigned_to
1191 end
1205 end
1192
1206
1193 def test_post_bulk_update_should_allow_fixed_version_to_be_set_to_a_subproject
1207 def test_post_bulk_update_should_allow_fixed_version_to_be_set_to_a_subproject
1194 @request.session[:user_id] = 2
1208 @request.session[:user_id] = 2
1195
1209
1196 post :bulk_update, :ids => [1,2], :issue => {:fixed_version_id => 4}
1210 post :bulk_update, :ids => [1,2], :issue => {:fixed_version_id => 4}
1197
1211
1198 assert_response :redirect
1212 assert_response :redirect
1199 issues = Issue.find([1,2])
1213 issues = Issue.find([1,2])
1200 issues.each do |issue|
1214 issues.each do |issue|
1201 assert_equal 4, issue.fixed_version_id
1215 assert_equal 4, issue.fixed_version_id
1202 assert_not_equal issue.project_id, issue.fixed_version.project_id
1216 assert_not_equal issue.project_id, issue.fixed_version.project_id
1203 end
1217 end
1204 end
1218 end
1205
1219
1206 def test_post_bulk_update_should_redirect_back_using_the_back_url_parameter
1220 def test_post_bulk_update_should_redirect_back_using_the_back_url_parameter
1207 @request.session[:user_id] = 2
1221 @request.session[:user_id] = 2
1208 post :bulk_update, :ids => [1,2], :back_url => '/issues'
1222 post :bulk_update, :ids => [1,2], :back_url => '/issues'
1209
1223
1210 assert_response :redirect
1224 assert_response :redirect
1211 assert_redirected_to '/issues'
1225 assert_redirected_to '/issues'
1212 end
1226 end
1213
1227
1214 def test_post_bulk_update_should_not_redirect_back_using_the_back_url_parameter_off_the_host
1228 def test_post_bulk_update_should_not_redirect_back_using_the_back_url_parameter_off_the_host
1215 @request.session[:user_id] = 2
1229 @request.session[:user_id] = 2
1216 post :bulk_update, :ids => [1,2], :back_url => 'http://google.com'
1230 post :bulk_update, :ids => [1,2], :back_url => 'http://google.com'
1217
1231
1218 assert_response :redirect
1232 assert_response :redirect
1219 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => Project.find(1).identifier
1233 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => Project.find(1).identifier
1220 end
1234 end
1221
1235
1222 def test_destroy_issue_with_no_time_entries
1236 def test_destroy_issue_with_no_time_entries
1223 assert_nil TimeEntry.find_by_issue_id(2)
1237 assert_nil TimeEntry.find_by_issue_id(2)
1224 @request.session[:user_id] = 2
1238 @request.session[:user_id] = 2
1225 post :destroy, :id => 2
1239 post :destroy, :id => 2
1226 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1240 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1227 assert_nil Issue.find_by_id(2)
1241 assert_nil Issue.find_by_id(2)
1228 end
1242 end
1229
1243
1230 def test_destroy_issues_with_time_entries
1244 def test_destroy_issues_with_time_entries
1231 @request.session[:user_id] = 2
1245 @request.session[:user_id] = 2
1232 post :destroy, :ids => [1, 3]
1246 post :destroy, :ids => [1, 3]
1233 assert_response :success
1247 assert_response :success
1234 assert_template 'destroy'
1248 assert_template 'destroy'
1235 assert_not_nil assigns(:hours)
1249 assert_not_nil assigns(:hours)
1236 assert Issue.find_by_id(1) && Issue.find_by_id(3)
1250 assert Issue.find_by_id(1) && Issue.find_by_id(3)
1237 end
1251 end
1238
1252
1239 def test_destroy_issues_and_destroy_time_entries
1253 def test_destroy_issues_and_destroy_time_entries
1240 @request.session[:user_id] = 2
1254 @request.session[:user_id] = 2
1241 post :destroy, :ids => [1, 3], :todo => 'destroy'
1255 post :destroy, :ids => [1, 3], :todo => 'destroy'
1242 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1256 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1243 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1257 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1244 assert_nil TimeEntry.find_by_id([1, 2])
1258 assert_nil TimeEntry.find_by_id([1, 2])
1245 end
1259 end
1246
1260
1247 def test_destroy_issues_and_assign_time_entries_to_project
1261 def test_destroy_issues_and_assign_time_entries_to_project
1248 @request.session[:user_id] = 2
1262 @request.session[:user_id] = 2
1249 post :destroy, :ids => [1, 3], :todo => 'nullify'
1263 post :destroy, :ids => [1, 3], :todo => 'nullify'
1250 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1264 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1251 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1265 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1252 assert_nil TimeEntry.find(1).issue_id
1266 assert_nil TimeEntry.find(1).issue_id
1253 assert_nil TimeEntry.find(2).issue_id
1267 assert_nil TimeEntry.find(2).issue_id
1254 end
1268 end
1255
1269
1256 def test_destroy_issues_and_reassign_time_entries_to_another_issue
1270 def test_destroy_issues_and_reassign_time_entries_to_another_issue
1257 @request.session[:user_id] = 2
1271 @request.session[:user_id] = 2
1258 post :destroy, :ids => [1, 3], :todo => 'reassign', :reassign_to_id => 2
1272 post :destroy, :ids => [1, 3], :todo => 'reassign', :reassign_to_id => 2
1259 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1273 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1260 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1274 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1261 assert_equal 2, TimeEntry.find(1).issue_id
1275 assert_equal 2, TimeEntry.find(1).issue_id
1262 assert_equal 2, TimeEntry.find(2).issue_id
1276 assert_equal 2, TimeEntry.find(2).issue_id
1263 end
1277 end
1264
1278
1265 def test_destroy_issues_from_different_projects
1279 def test_destroy_issues_from_different_projects
1266 @request.session[:user_id] = 2
1280 @request.session[:user_id] = 2
1267 post :destroy, :ids => [1, 2, 6], :todo => 'destroy'
1281 post :destroy, :ids => [1, 2, 6], :todo => 'destroy'
1268 assert_redirected_to :controller => 'issues', :action => 'index'
1282 assert_redirected_to :controller => 'issues', :action => 'index'
1269 assert !(Issue.find_by_id(1) || Issue.find_by_id(2) || Issue.find_by_id(6))
1283 assert !(Issue.find_by_id(1) || Issue.find_by_id(2) || Issue.find_by_id(6))
1270 end
1284 end
1271
1285
1272 def test_default_search_scope
1286 def test_default_search_scope
1273 get :index
1287 get :index
1274 assert_tag :div, :attributes => {:id => 'quick-search'},
1288 assert_tag :div, :attributes => {:id => 'quick-search'},
1275 :child => {:tag => 'form',
1289 :child => {:tag => 'form',
1276 :child => {:tag => 'input', :attributes => {:name => 'issues', :type => 'hidden', :value => '1'}}}
1290 :child => {:tag => 'input', :attributes => {:name => 'issues', :type => 'hidden', :value => '1'}}}
1277 end
1291 end
1278 end
1292 end
General Comments 0
You need to be logged in to leave comments. Login now