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