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