##// END OF EJS Templates
Merged r4414 from trunk....
Jean-Philippe Lang -
r4301:dbb26b08f804
parent child
Show More
@@ -1,840 +1,840
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 :close_duplicates, :update_done_ratio_from_issue_status
71 before_save :close_duplicates, :update_done_ratio_from_issue_status
72 after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal
72 after_save :reschedule_following_issues, :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.visible.find(arg)
95 issue = arg.is_a?(Issue) ? arg : Issue.visible.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'].to_i)
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 # Does this issue have children?
361 # Does this issue have children?
362 def children?
362 def children?
363 !leaf?
363 !leaf?
364 end
364 end
365
365
366 # Users the issue can be assigned to
366 # Users the issue can be assigned to
367 def assignable_users
367 def assignable_users
368 users = project.assignable_users
368 users = project.assignable_users
369 users << author if author
369 users << author if author
370 users.uniq.sort
370 users.uniq.sort
371 end
371 end
372
372
373 # Versions that the issue can be assigned to
373 # Versions that the issue can be assigned to
374 def assignable_versions
374 def assignable_versions
375 @assignable_versions ||= (project.shared_versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort
375 @assignable_versions ||= (project.shared_versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort
376 end
376 end
377
377
378 # Returns true if this issue is blocked by another issue that is still open
378 # Returns true if this issue is blocked by another issue that is still open
379 def blocked?
379 def blocked?
380 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
380 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
381 end
381 end
382
382
383 # Returns an array of status that user is able to apply
383 # Returns an array of status that user is able to apply
384 def new_statuses_allowed_to(user, include_default=false)
384 def new_statuses_allowed_to(user, include_default=false)
385 statuses = status.find_new_statuses_allowed_to(user.roles_for_project(project), tracker)
385 statuses = status.find_new_statuses_allowed_to(user.roles_for_project(project), tracker)
386 statuses << status unless statuses.empty?
386 statuses << status unless statuses.empty?
387 statuses << IssueStatus.default if include_default
387 statuses << IssueStatus.default if include_default
388 statuses = statuses.uniq.sort
388 statuses = statuses.uniq.sort
389 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
389 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
390 end
390 end
391
391
392 # Returns the mail adresses of users that should be notified
392 # Returns the mail adresses of users that should be notified
393 def recipients
393 def recipients
394 notified = project.notified_users
394 notified = project.notified_users
395 # Author and assignee are always notified unless they have been locked
395 # Author and assignee are always notified unless they have been locked
396 notified << author if author && author.active?
396 notified << author if author && author.active?
397 notified << assigned_to if assigned_to && assigned_to.active?
397 notified << assigned_to if assigned_to && assigned_to.active?
398 notified.uniq!
398 notified.uniq!
399 # Remove users that can not view the issue
399 # Remove users that can not view the issue
400 notified.reject! {|user| !visible?(user)}
400 notified.reject! {|user| !visible?(user)}
401 notified.collect(&:mail)
401 notified.collect(&:mail)
402 end
402 end
403
403
404 # Returns the total number of hours spent on this issue and its descendants
404 # Returns the total number of hours spent on this issue and its descendants
405 #
405 #
406 # Example:
406 # Example:
407 # spent_hours => 0.0
407 # spent_hours => 0.0
408 # spent_hours => 50.2
408 # spent_hours => 50.2
409 def spent_hours
409 def spent_hours
410 @spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours", :include => :time_entries).to_f || 0.0
410 @spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours", :include => :time_entries).to_f || 0.0
411 end
411 end
412
412
413 def relations
413 def relations
414 (relations_from + relations_to).sort
414 (relations_from + relations_to).sort
415 end
415 end
416
416
417 def all_dependent_issues
417 def all_dependent_issues
418 dependencies = []
418 dependencies = []
419 relations_from.each do |relation|
419 relations_from.each do |relation|
420 dependencies << relation.issue_to
420 dependencies << relation.issue_to
421 dependencies += relation.issue_to.all_dependent_issues
421 dependencies += relation.issue_to.all_dependent_issues
422 end
422 end
423 dependencies
423 dependencies
424 end
424 end
425
425
426 # Returns an array of issues that duplicate this one
426 # Returns an array of issues that duplicate this one
427 def duplicates
427 def duplicates
428 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
428 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
429 end
429 end
430
430
431 # Returns the due date or the target due date if any
431 # Returns the due date or the target due date if any
432 # Used on gantt chart
432 # Used on gantt chart
433 def due_before
433 def due_before
434 due_date || (fixed_version ? fixed_version.effective_date : nil)
434 due_date || (fixed_version ? fixed_version.effective_date : nil)
435 end
435 end
436
436
437 # Returns the time scheduled for this issue.
437 # Returns the time scheduled for this issue.
438 #
438 #
439 # Example:
439 # Example:
440 # Start Date: 2/26/09, End Date: 3/04/09
440 # Start Date: 2/26/09, End Date: 3/04/09
441 # duration => 6
441 # duration => 6
442 def duration
442 def duration
443 (start_date && due_date) ? due_date - start_date : 0
443 (start_date && due_date) ? due_date - start_date : 0
444 end
444 end
445
445
446 def soonest_start
446 def soonest_start
447 @soonest_start ||= (
447 @soonest_start ||= (
448 relations_to.collect{|relation| relation.successor_soonest_start} +
448 relations_to.collect{|relation| relation.successor_soonest_start} +
449 ancestors.collect(&:soonest_start)
449 ancestors.collect(&:soonest_start)
450 ).compact.max
450 ).compact.max
451 end
451 end
452
452
453 def reschedule_after(date)
453 def reschedule_after(date)
454 return if date.nil?
454 return if date.nil?
455 if leaf?
455 if leaf?
456 if start_date.nil? || start_date < date
456 if start_date.nil? || start_date < date
457 self.start_date, self.due_date = date, date + duration
457 self.start_date, self.due_date = date, date + duration
458 save
458 save
459 end
459 end
460 else
460 else
461 leaves.each do |leaf|
461 leaves.each do |leaf|
462 leaf.reschedule_after(date)
462 leaf.reschedule_after(date)
463 end
463 end
464 end
464 end
465 end
465 end
466
466
467 def <=>(issue)
467 def <=>(issue)
468 if issue.nil?
468 if issue.nil?
469 -1
469 -1
470 elsif root_id != issue.root_id
470 elsif root_id != issue.root_id
471 (root_id || 0) <=> (issue.root_id || 0)
471 (root_id || 0) <=> (issue.root_id || 0)
472 else
472 else
473 (lft || 0) <=> (issue.lft || 0)
473 (lft || 0) <=> (issue.lft || 0)
474 end
474 end
475 end
475 end
476
476
477 def to_s
477 def to_s
478 "#{tracker} ##{id}: #{subject}"
478 "#{tracker} ##{id}: #{subject}"
479 end
479 end
480
480
481 # Returns a string of css classes that apply to the issue
481 # Returns a string of css classes that apply to the issue
482 def css_classes
482 def css_classes
483 s = "issue status-#{status.position} priority-#{priority.position}"
483 s = "issue status-#{status.position} priority-#{priority.position}"
484 s << ' closed' if closed?
484 s << ' closed' if closed?
485 s << ' overdue' if overdue?
485 s << ' overdue' if overdue?
486 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
486 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
487 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
487 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
488 s
488 s
489 end
489 end
490
490
491 # Saves an issue, time_entry, attachments, and a journal from the parameters
491 # Saves an issue, time_entry, attachments, and a journal from the parameters
492 # Returns false if save fails
492 # Returns false if save fails
493 def save_issue_with_child_records(params, existing_time_entry=nil)
493 def save_issue_with_child_records(params, existing_time_entry=nil)
494 Issue.transaction do
494 Issue.transaction do
495 if params[:time_entry] && params[:time_entry][:hours].present? && User.current.allowed_to?(:log_time, project)
495 if params[:time_entry] && params[:time_entry][:hours].present? && User.current.allowed_to?(:log_time, project)
496 @time_entry = existing_time_entry || TimeEntry.new
496 @time_entry = existing_time_entry || TimeEntry.new
497 @time_entry.project = project
497 @time_entry.project = project
498 @time_entry.issue = self
498 @time_entry.issue = self
499 @time_entry.user = User.current
499 @time_entry.user = User.current
500 @time_entry.spent_on = Date.today
500 @time_entry.spent_on = Date.today
501 @time_entry.attributes = params[:time_entry]
501 @time_entry.attributes = params[:time_entry]
502 self.time_entries << @time_entry
502 self.time_entries << @time_entry
503 end
503 end
504
504
505 if valid?
505 if valid?
506 attachments = Attachment.attach_files(self, params[:attachments])
506 attachments = Attachment.attach_files(self, params[:attachments])
507
507
508 attachments[:files].each {|a| @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
508 attachments[:files].each {|a| @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
509 # TODO: Rename hook
509 # TODO: Rename hook
510 Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
510 Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
511 begin
511 begin
512 if save
512 if save
513 # TODO: Rename hook
513 # TODO: Rename hook
514 Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
514 Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
515 else
515 else
516 raise ActiveRecord::Rollback
516 raise ActiveRecord::Rollback
517 end
517 end
518 rescue ActiveRecord::StaleObjectError
518 rescue ActiveRecord::StaleObjectError
519 attachments[:files].each(&:destroy)
519 attachments[:files].each(&:destroy)
520 errors.add_to_base l(:notice_locking_conflict)
520 errors.add_to_base l(:notice_locking_conflict)
521 raise ActiveRecord::Rollback
521 raise ActiveRecord::Rollback
522 end
522 end
523 end
523 end
524 end
524 end
525 end
525 end
526
526
527 # Unassigns issues from +version+ if it's no longer shared with issue's project
527 # Unassigns issues from +version+ if it's no longer shared with issue's project
528 def self.update_versions_from_sharing_change(version)
528 def self.update_versions_from_sharing_change(version)
529 # Update issues assigned to the version
529 # Update issues assigned to the version
530 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
530 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
531 end
531 end
532
532
533 # Unassigns issues from versions that are no longer shared
533 # Unassigns issues from versions that are no longer shared
534 # after +project+ was moved
534 # after +project+ was moved
535 def self.update_versions_from_hierarchy_change(project)
535 def self.update_versions_from_hierarchy_change(project)
536 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
536 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
537 # Update issues of the moved projects and issues assigned to a version of a moved project
537 # Update issues of the moved projects and issues assigned to a version of a moved project
538 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
538 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
539 end
539 end
540
540
541 def parent_issue_id=(arg)
541 def parent_issue_id=(arg)
542 parent_issue_id = arg.blank? ? nil : arg.to_i
542 parent_issue_id = arg.blank? ? nil : arg.to_i
543 if parent_issue_id && @parent_issue = Issue.find_by_id(parent_issue_id)
543 if parent_issue_id && @parent_issue = Issue.find_by_id(parent_issue_id)
544 @parent_issue.id
544 @parent_issue.id
545 else
545 else
546 @parent_issue = nil
546 @parent_issue = nil
547 nil
547 nil
548 end
548 end
549 end
549 end
550
550
551 def parent_issue_id
551 def parent_issue_id
552 if instance_variable_defined? :@parent_issue
552 if instance_variable_defined? :@parent_issue
553 @parent_issue.nil? ? nil : @parent_issue.id
553 @parent_issue.nil? ? nil : @parent_issue.id
554 else
554 else
555 parent_id
555 parent_id
556 end
556 end
557 end
557 end
558
558
559 # Extracted from the ReportsController.
559 # Extracted from the ReportsController.
560 def self.by_tracker(project)
560 def self.by_tracker(project)
561 count_and_group_by(:project => project,
561 count_and_group_by(:project => project,
562 :field => 'tracker_id',
562 :field => 'tracker_id',
563 :joins => Tracker.table_name)
563 :joins => Tracker.table_name)
564 end
564 end
565
565
566 def self.by_version(project)
566 def self.by_version(project)
567 count_and_group_by(:project => project,
567 count_and_group_by(:project => project,
568 :field => 'fixed_version_id',
568 :field => 'fixed_version_id',
569 :joins => Version.table_name)
569 :joins => Version.table_name)
570 end
570 end
571
571
572 def self.by_priority(project)
572 def self.by_priority(project)
573 count_and_group_by(:project => project,
573 count_and_group_by(:project => project,
574 :field => 'priority_id',
574 :field => 'priority_id',
575 :joins => IssuePriority.table_name)
575 :joins => IssuePriority.table_name)
576 end
576 end
577
577
578 def self.by_category(project)
578 def self.by_category(project)
579 count_and_group_by(:project => project,
579 count_and_group_by(:project => project,
580 :field => 'category_id',
580 :field => 'category_id',
581 :joins => IssueCategory.table_name)
581 :joins => IssueCategory.table_name)
582 end
582 end
583
583
584 def self.by_assigned_to(project)
584 def self.by_assigned_to(project)
585 count_and_group_by(:project => project,
585 count_and_group_by(:project => project,
586 :field => 'assigned_to_id',
586 :field => 'assigned_to_id',
587 :joins => User.table_name)
587 :joins => User.table_name)
588 end
588 end
589
589
590 def self.by_author(project)
590 def self.by_author(project)
591 count_and_group_by(:project => project,
591 count_and_group_by(:project => project,
592 :field => 'author_id',
592 :field => 'author_id',
593 :joins => User.table_name)
593 :joins => User.table_name)
594 end
594 end
595
595
596 def self.by_subproject(project)
596 def self.by_subproject(project)
597 ActiveRecord::Base.connection.select_all("select s.id as status_id,
597 ActiveRecord::Base.connection.select_all("select s.id as status_id,
598 s.is_closed as closed,
598 s.is_closed as closed,
599 i.project_id as project_id,
599 i.project_id as project_id,
600 count(i.id) as total
600 count(i.id) as total
601 from
601 from
602 #{Issue.table_name} i, #{IssueStatus.table_name} s
602 #{Issue.table_name} i, #{IssueStatus.table_name} s
603 where
603 where
604 i.status_id=s.id
604 i.status_id=s.id
605 and i.project_id IN (#{project.descendants.active.collect{|p| p.id}.join(',')})
605 and i.project_id IN (#{project.descendants.active.collect{|p| p.id}.join(',')})
606 group by s.id, s.is_closed, i.project_id") if project.descendants.active.any?
606 group by s.id, s.is_closed, i.project_id") if project.descendants.active.any?
607 end
607 end
608 # End ReportsController extraction
608 # End ReportsController extraction
609
609
610 # Returns an array of projects that current user can move issues to
610 # Returns an array of projects that current user can move issues to
611 def self.allowed_target_projects_on_move
611 def self.allowed_target_projects_on_move
612 projects = []
612 projects = []
613 if User.current.admin?
613 if User.current.admin?
614 # admin is allowed to move issues to any active (visible) project
614 # admin is allowed to move issues to any active (visible) project
615 projects = Project.visible.all
615 projects = Project.visible.all
616 elsif User.current.logged?
616 elsif User.current.logged?
617 if Role.non_member.allowed_to?(:move_issues)
617 if Role.non_member.allowed_to?(:move_issues)
618 projects = Project.visible.all
618 projects = Project.visible.all
619 else
619 else
620 User.current.memberships.each {|m| projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}}
620 User.current.memberships.each {|m| projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}}
621 end
621 end
622 end
622 end
623 projects
623 projects
624 end
624 end
625
625
626 private
626 private
627
627
628 def update_nested_set_attributes
628 def update_nested_set_attributes
629 if root_id.nil?
629 if root_id.nil?
630 # issue was just created
630 # issue was just created
631 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
631 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
632 set_default_left_and_right
632 set_default_left_and_right
633 Issue.update_all("root_id = #{root_id}, lft = #{lft}, rgt = #{rgt}", ["id = ?", id])
633 Issue.update_all("root_id = #{root_id}, lft = #{lft}, rgt = #{rgt}", ["id = ?", id])
634 if @parent_issue
634 if @parent_issue
635 move_to_child_of(@parent_issue)
635 move_to_child_of(@parent_issue)
636 end
636 end
637 reload
637 reload
638 elsif parent_issue_id != parent_id
638 elsif parent_issue_id != parent_id
639 former_parent_id = parent_id
639 former_parent_id = parent_id
640 # moving an existing issue
640 # moving an existing issue
641 if @parent_issue && @parent_issue.root_id == root_id
641 if @parent_issue && @parent_issue.root_id == root_id
642 # inside the same tree
642 # inside the same tree
643 move_to_child_of(@parent_issue)
643 move_to_child_of(@parent_issue)
644 else
644 else
645 # to another tree
645 # to another tree
646 unless root?
646 unless root?
647 move_to_right_of(root)
647 move_to_right_of(root)
648 reload
648 reload
649 end
649 end
650 old_root_id = root_id
650 old_root_id = root_id
651 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
651 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
652 target_maxright = nested_set_scope.maximum(right_column_name) || 0
652 target_maxright = nested_set_scope.maximum(right_column_name) || 0
653 offset = target_maxright + 1 - lft
653 offset = target_maxright + 1 - lft
654 Issue.update_all("root_id = #{root_id}, lft = lft + #{offset}, rgt = rgt + #{offset}",
654 Issue.update_all("root_id = #{root_id}, lft = lft + #{offset}, rgt = rgt + #{offset}",
655 ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
655 ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
656 self[left_column_name] = lft + offset
656 self[left_column_name] = lft + offset
657 self[right_column_name] = rgt + offset
657 self[right_column_name] = rgt + offset
658 if @parent_issue
658 if @parent_issue
659 move_to_child_of(@parent_issue)
659 move_to_child_of(@parent_issue)
660 end
660 end
661 end
661 end
662 reload
662 reload
663 # delete invalid relations of all descendants
663 # delete invalid relations of all descendants
664 self_and_descendants.each do |issue|
664 self_and_descendants.each do |issue|
665 issue.relations.each do |relation|
665 issue.relations.each do |relation|
666 relation.destroy unless relation.valid?
666 relation.destroy unless relation.valid?
667 end
667 end
668 end
668 end
669 # update former parent
669 # update former parent
670 recalculate_attributes_for(former_parent_id) if former_parent_id
670 recalculate_attributes_for(former_parent_id) if former_parent_id
671 end
671 end
672 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
672 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
673 end
673 end
674
674
675 def update_parent_attributes
675 def update_parent_attributes
676 recalculate_attributes_for(parent_id) if parent_id
676 recalculate_attributes_for(parent_id) if parent_id
677 end
677 end
678
678
679 def recalculate_attributes_for(issue_id)
679 def recalculate_attributes_for(issue_id)
680 if issue_id && p = Issue.find_by_id(issue_id)
680 if issue_id && p = Issue.find_by_id(issue_id)
681 # priority = highest priority of children
681 # priority = highest priority of children
682 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :include => :priority)
682 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :include => :priority)
683 p.priority = IssuePriority.find_by_position(priority_position)
683 p.priority = IssuePriority.find_by_position(priority_position)
684 end
684 end
685
685
686 # start/due dates = lowest/highest dates of children
686 # start/due dates = lowest/highest dates of children
687 p.start_date = p.children.minimum(:start_date)
687 p.start_date = p.children.minimum(:start_date)
688 p.due_date = p.children.maximum(:due_date)
688 p.due_date = p.children.maximum(:due_date)
689 if p.start_date && p.due_date && p.due_date < p.start_date
689 if p.start_date && p.due_date && p.due_date < p.start_date
690 p.start_date, p.due_date = p.due_date, p.start_date
690 p.start_date, p.due_date = p.due_date, p.start_date
691 end
691 end
692
692
693 # done ratio = weighted average ratio of leaves
693 # done ratio = weighted average ratio of leaves
694 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
694 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
695 leaves_count = p.leaves.count
695 leaves_count = p.leaves.count
696 if leaves_count > 0
696 if leaves_count > 0
697 average = p.leaves.average(:estimated_hours).to_f
697 average = p.leaves.average(:estimated_hours).to_f
698 if average == 0
698 if average == 0
699 average = 1
699 average = 1
700 end
700 end
701 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
701 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
702 progress = done / (average * leaves_count)
702 progress = done / (average * leaves_count)
703 p.done_ratio = progress.round
703 p.done_ratio = progress.round
704 end
704 end
705 end
705 end
706
706
707 # estimate = sum of leaves estimates
707 # estimate = sum of leaves estimates
708 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
708 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
709 p.estimated_hours = nil if p.estimated_hours == 0.0
709 p.estimated_hours = nil if p.estimated_hours == 0.0
710
710
711 # ancestors will be recursively updated
711 # ancestors will be recursively updated
712 p.save(false)
712 p.save(false)
713 end
713 end
714 end
714 end
715
715
716 def destroy_children
716 def destroy_children
717 unless leaf?
717 unless leaf?
718 children.each do |child|
718 children.each do |child|
719 child.destroy
719 child.destroy
720 end
720 end
721 end
721 end
722 end
722 end
723
723
724 # Update issues so their versions are not pointing to a
724 # Update issues so their versions are not pointing to a
725 # fixed_version that is not shared with the issue's project
725 # fixed_version that is not shared with the issue's project
726 def self.update_versions(conditions=nil)
726 def self.update_versions(conditions=nil)
727 # Only need to update issues with a fixed_version from
727 # Only need to update issues with a fixed_version from
728 # a different project and that is not systemwide shared
728 # a different project and that is not systemwide shared
729 Issue.all(:conditions => merge_conditions("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
729 Issue.all(:conditions => merge_conditions("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
730 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
730 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
731 " AND #{Version.table_name}.sharing <> 'system'",
731 " AND #{Version.table_name}.sharing <> 'system'",
732 conditions),
732 conditions),
733 :include => [:project, :fixed_version]
733 :include => [:project, :fixed_version]
734 ).each do |issue|
734 ).each do |issue|
735 next if issue.project.nil? || issue.fixed_version.nil?
735 next if issue.project.nil? || issue.fixed_version.nil?
736 unless issue.project.shared_versions.include?(issue.fixed_version)
736 unless issue.project.shared_versions.include?(issue.fixed_version)
737 issue.init_journal(User.current)
737 issue.init_journal(User.current)
738 issue.fixed_version = nil
738 issue.fixed_version = nil
739 issue.save
739 issue.save
740 end
740 end
741 end
741 end
742 end
742 end
743
743
744 # Callback on attachment deletion
744 # Callback on attachment deletion
745 def attachment_removed(obj)
745 def attachment_removed(obj)
746 journal = init_journal(User.current)
746 journal = init_journal(User.current)
747 journal.details << JournalDetail.new(:property => 'attachment',
747 journal.details << JournalDetail.new(:property => 'attachment',
748 :prop_key => obj.id,
748 :prop_key => obj.id,
749 :old_value => obj.filename)
749 :old_value => obj.filename)
750 journal.save
750 journal.save
751 end
751 end
752
752
753 # Default assignment based on category
753 # Default assignment based on category
754 def default_assign
754 def default_assign
755 if assigned_to.nil? && category && category.assigned_to
755 if assigned_to.nil? && category && category.assigned_to
756 self.assigned_to = category.assigned_to
756 self.assigned_to = category.assigned_to
757 end
757 end
758 end
758 end
759
759
760 # Updates start/due dates of following issues
760 # Updates start/due dates of following issues
761 def reschedule_following_issues
761 def reschedule_following_issues
762 if start_date_changed? || due_date_changed?
762 if start_date_changed? || due_date_changed?
763 relations_from.each do |relation|
763 relations_from.each do |relation|
764 relation.set_issue_to_dates
764 relation.set_issue_to_dates
765 end
765 end
766 end
766 end
767 end
767 end
768
768
769 # Closes duplicates if the issue is being closed
769 # Closes duplicates if the issue is being closed
770 def close_duplicates
770 def close_duplicates
771 if closing?
771 if closing?
772 duplicates.each do |duplicate|
772 duplicates.each do |duplicate|
773 # Reload is need in case the duplicate was updated by a previous duplicate
773 # Reload is need in case the duplicate was updated by a previous duplicate
774 duplicate.reload
774 duplicate.reload
775 # Don't re-close it if it's already closed
775 # Don't re-close it if it's already closed
776 next if duplicate.closed?
776 next if duplicate.closed?
777 # Same user and notes
777 # Same user and notes
778 if @current_journal
778 if @current_journal
779 duplicate.init_journal(@current_journal.user, @current_journal.notes)
779 duplicate.init_journal(@current_journal.user, @current_journal.notes)
780 end
780 end
781 duplicate.update_attribute :status, self.status
781 duplicate.update_attribute :status, self.status
782 end
782 end
783 end
783 end
784 end
784 end
785
785
786 # Saves the changes in a Journal
786 # Saves the changes in a Journal
787 # Called after_save
787 # Called after_save
788 def create_journal
788 def create_journal
789 if @current_journal
789 if @current_journal
790 # attributes changes
790 # attributes changes
791 (Issue.column_names - %w(id description root_id lft rgt lock_version created_on updated_on)).each {|c|
791 (Issue.column_names - %w(id description root_id lft rgt lock_version created_on updated_on)).each {|c|
792 @current_journal.details << JournalDetail.new(:property => 'attr',
792 @current_journal.details << JournalDetail.new(:property => 'attr',
793 :prop_key => c,
793 :prop_key => c,
794 :old_value => @issue_before_change.send(c),
794 :old_value => @issue_before_change.send(c),
795 :value => send(c)) unless send(c)==@issue_before_change.send(c)
795 :value => send(c)) unless send(c)==@issue_before_change.send(c)
796 }
796 }
797 # custom fields changes
797 # custom fields changes
798 custom_values.each {|c|
798 custom_values.each {|c|
799 next if (@custom_values_before_change[c.custom_field_id]==c.value ||
799 next if (@custom_values_before_change[c.custom_field_id]==c.value ||
800 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
800 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
801 @current_journal.details << JournalDetail.new(:property => 'cf',
801 @current_journal.details << JournalDetail.new(:property => 'cf',
802 :prop_key => c.custom_field_id,
802 :prop_key => c.custom_field_id,
803 :old_value => @custom_values_before_change[c.custom_field_id],
803 :old_value => @custom_values_before_change[c.custom_field_id],
804 :value => c.value)
804 :value => c.value)
805 }
805 }
806 @current_journal.save
806 @current_journal.save
807 # reset current journal
807 # reset current journal
808 init_journal @current_journal.user, @current_journal.notes
808 init_journal @current_journal.user, @current_journal.notes
809 end
809 end
810 end
810 end
811
811
812 # Query generator for selecting groups of issue counts for a project
812 # Query generator for selecting groups of issue counts for a project
813 # based on specific criteria
813 # based on specific criteria
814 #
814 #
815 # Options
815 # Options
816 # * project - Project to search in.
816 # * project - Project to search in.
817 # * field - String. Issue field to key off of in the grouping.
817 # * field - String. Issue field to key off of in the grouping.
818 # * joins - String. The table name to join against.
818 # * joins - String. The table name to join against.
819 def self.count_and_group_by(options)
819 def self.count_and_group_by(options)
820 project = options.delete(:project)
820 project = options.delete(:project)
821 select_field = options.delete(:field)
821 select_field = options.delete(:field)
822 joins = options.delete(:joins)
822 joins = options.delete(:joins)
823
823
824 where = "i.#{select_field}=j.id"
824 where = "i.#{select_field}=j.id"
825
825
826 ActiveRecord::Base.connection.select_all("select s.id as status_id,
826 ActiveRecord::Base.connection.select_all("select s.id as status_id,
827 s.is_closed as closed,
827 s.is_closed as closed,
828 j.id as #{select_field},
828 j.id as #{select_field},
829 count(i.id) as total
829 count(i.id) as total
830 from
830 from
831 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{joins} j
831 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{joins} j
832 where
832 where
833 i.status_id=s.id
833 i.status_id=s.id
834 and #{where}
834 and #{where}
835 and i.project_id=#{project.id}
835 and i.project_id=#{project.id}
836 group by s.id, s.is_closed, j.id")
836 group by s.id, s.is_closed, j.id")
837 end
837 end
838
838
839
839
840 end
840 end
@@ -1,1092 +1,1106
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_show_by_anonymous
234 def test_show_by_anonymous
235 get :show, :id => 1
235 get :show, :id => 1
236 assert_response :success
236 assert_response :success
237 assert_template 'show.rhtml'
237 assert_template 'show.rhtml'
238 assert_not_nil assigns(:issue)
238 assert_not_nil assigns(:issue)
239 assert_equal Issue.find(1), assigns(:issue)
239 assert_equal Issue.find(1), assigns(:issue)
240
240
241 # anonymous role is allowed to add a note
241 # anonymous role is allowed to add a note
242 assert_tag :tag => 'form',
242 assert_tag :tag => 'form',
243 :descendant => { :tag => 'fieldset',
243 :descendant => { :tag => 'fieldset',
244 :child => { :tag => 'legend',
244 :child => { :tag => 'legend',
245 :content => /Notes/ } }
245 :content => /Notes/ } }
246 end
246 end
247
247
248 def test_show_by_manager
248 def test_show_by_manager
249 @request.session[:user_id] = 2
249 @request.session[:user_id] = 2
250 get :show, :id => 1
250 get :show, :id => 1
251 assert_response :success
251 assert_response :success
252
252
253 assert_tag :tag => 'form',
253 assert_tag :tag => 'form',
254 :descendant => { :tag => 'fieldset',
254 :descendant => { :tag => 'fieldset',
255 :child => { :tag => 'legend',
255 :child => { :tag => 'legend',
256 :content => /Change properties/ } },
256 :content => /Change properties/ } },
257 :descendant => { :tag => 'fieldset',
257 :descendant => { :tag => 'fieldset',
258 :child => { :tag => 'legend',
258 :child => { :tag => 'legend',
259 :content => /Log time/ } },
259 :content => /Log time/ } },
260 :descendant => { :tag => 'fieldset',
260 :descendant => { :tag => 'fieldset',
261 :child => { :tag => 'legend',
261 :child => { :tag => 'legend',
262 :content => /Notes/ } }
262 :content => /Notes/ } }
263 end
263 end
264
264
265 def test_show_should_deny_anonymous_access_without_permission
265 def test_show_should_deny_anonymous_access_without_permission
266 Role.anonymous.remove_permission!(:view_issues)
266 Role.anonymous.remove_permission!(:view_issues)
267 get :show, :id => 1
267 get :show, :id => 1
268 assert_response :redirect
268 assert_response :redirect
269 end
269 end
270
270
271 def test_show_should_deny_non_member_access_without_permission
271 def test_show_should_deny_non_member_access_without_permission
272 Role.non_member.remove_permission!(:view_issues)
272 Role.non_member.remove_permission!(:view_issues)
273 @request.session[:user_id] = 9
273 @request.session[:user_id] = 9
274 get :show, :id => 1
274 get :show, :id => 1
275 assert_response 403
275 assert_response 403
276 end
276 end
277
277
278 def test_show_should_deny_member_access_without_permission
278 def test_show_should_deny_member_access_without_permission
279 Role.find(1).remove_permission!(:view_issues)
279 Role.find(1).remove_permission!(:view_issues)
280 @request.session[:user_id] = 2
280 @request.session[:user_id] = 2
281 get :show, :id => 1
281 get :show, :id => 1
282 assert_response 403
282 assert_response 403
283 end
283 end
284
284
285 def test_show_should_not_disclose_relations_to_invisible_issues
285 def test_show_should_not_disclose_relations_to_invisible_issues
286 Setting.cross_project_issue_relations = '1'
286 Setting.cross_project_issue_relations = '1'
287 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(2), :relation_type => 'relates')
287 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(2), :relation_type => 'relates')
288 # Relation to a private project issue
288 # Relation to a private project issue
289 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(4), :relation_type => 'relates')
289 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(4), :relation_type => 'relates')
290
290
291 get :show, :id => 1
291 get :show, :id => 1
292 assert_response :success
292 assert_response :success
293
293
294 assert_tag :div, :attributes => { :id => 'relations' },
294 assert_tag :div, :attributes => { :id => 'relations' },
295 :descendant => { :tag => 'a', :content => /#2$/ }
295 :descendant => { :tag => 'a', :content => /#2$/ }
296 assert_no_tag :div, :attributes => { :id => 'relations' },
296 assert_no_tag :div, :attributes => { :id => 'relations' },
297 :descendant => { :tag => 'a', :content => /#4$/ }
297 :descendant => { :tag => 'a', :content => /#4$/ }
298 end
298 end
299
299
300 def test_show_atom
300 def test_show_atom
301 get :show, :id => 2, :format => 'atom'
301 get :show, :id => 2, :format => 'atom'
302 assert_response :success
302 assert_response :success
303 assert_template 'journals/index.rxml'
303 assert_template 'journals/index.rxml'
304 # Inline image
304 # Inline image
305 assert_select 'content', :text => Regexp.new(Regexp.quote('http://test.host/attachments/download/10'))
305 assert_select 'content', :text => Regexp.new(Regexp.quote('http://test.host/attachments/download/10'))
306 end
306 end
307
307
308 def test_show_export_to_pdf
308 def test_show_export_to_pdf
309 get :show, :id => 3, :format => 'pdf'
309 get :show, :id => 3, :format => 'pdf'
310 assert_response :success
310 assert_response :success
311 assert_equal 'application/pdf', @response.content_type
311 assert_equal 'application/pdf', @response.content_type
312 assert @response.body.starts_with?('%PDF')
312 assert @response.body.starts_with?('%PDF')
313 assert_not_nil assigns(:issue)
313 assert_not_nil assigns(:issue)
314 end
314 end
315
315
316 def test_get_new
316 def test_get_new
317 @request.session[:user_id] = 2
317 @request.session[:user_id] = 2
318 get :new, :project_id => 1, :tracker_id => 1
318 get :new, :project_id => 1, :tracker_id => 1
319 assert_response :success
319 assert_response :success
320 assert_template 'new'
320 assert_template 'new'
321
321
322 assert_tag :tag => 'input', :attributes => { :name => 'issue[custom_field_values][2]',
322 assert_tag :tag => 'input', :attributes => { :name => 'issue[custom_field_values][2]',
323 :value => 'Default string' }
323 :value => 'Default string' }
324 end
324 end
325
325
326 def test_get_new_without_tracker_id
326 def test_get_new_without_tracker_id
327 @request.session[:user_id] = 2
327 @request.session[:user_id] = 2
328 get :new, :project_id => 1
328 get :new, :project_id => 1
329 assert_response :success
329 assert_response :success
330 assert_template 'new'
330 assert_template 'new'
331
331
332 issue = assigns(:issue)
332 issue = assigns(:issue)
333 assert_not_nil issue
333 assert_not_nil issue
334 assert_equal Project.find(1).trackers.first, issue.tracker
334 assert_equal Project.find(1).trackers.first, issue.tracker
335 end
335 end
336
336
337 def test_get_new_with_no_default_status_should_display_an_error
337 def test_get_new_with_no_default_status_should_display_an_error
338 @request.session[:user_id] = 2
338 @request.session[:user_id] = 2
339 IssueStatus.delete_all
339 IssueStatus.delete_all
340
340
341 get :new, :project_id => 1
341 get :new, :project_id => 1
342 assert_response 500
342 assert_response 500
343 assert_not_nil flash[:error]
343 assert_not_nil flash[:error]
344 assert_tag :tag => 'div', :attributes => { :class => /error/ },
344 assert_tag :tag => 'div', :attributes => { :class => /error/ },
345 :content => /No default issue/
345 :content => /No default issue/
346 end
346 end
347
347
348 def test_get_new_with_no_tracker_should_display_an_error
348 def test_get_new_with_no_tracker_should_display_an_error
349 @request.session[:user_id] = 2
349 @request.session[:user_id] = 2
350 Tracker.delete_all
350 Tracker.delete_all
351
351
352 get :new, :project_id => 1
352 get :new, :project_id => 1
353 assert_response 500
353 assert_response 500
354 assert_not_nil flash[:error]
354 assert_not_nil flash[:error]
355 assert_tag :tag => 'div', :attributes => { :class => /error/ },
355 assert_tag :tag => 'div', :attributes => { :class => /error/ },
356 :content => /No tracker/
356 :content => /No tracker/
357 end
357 end
358
358
359 def test_update_new_form
359 def test_update_new_form
360 @request.session[:user_id] = 2
360 @request.session[:user_id] = 2
361 xhr :post, :new, :project_id => 1,
361 xhr :post, :new, :project_id => 1,
362 :issue => {:tracker_id => 2,
362 :issue => {:tracker_id => 2,
363 :subject => 'This is the test_new issue',
363 :subject => 'This is the test_new issue',
364 :description => 'This is the description',
364 :description => 'This is the description',
365 :priority_id => 5}
365 :priority_id => 5}
366 assert_response :success
366 assert_response :success
367 assert_template 'attributes'
367 assert_template 'attributes'
368
368
369 issue = assigns(:issue)
369 issue = assigns(:issue)
370 assert_kind_of Issue, issue
370 assert_kind_of Issue, issue
371 assert_equal 1, issue.project_id
371 assert_equal 1, issue.project_id
372 assert_equal 2, issue.tracker_id
372 assert_equal 2, issue.tracker_id
373 assert_equal 'This is the test_new issue', issue.subject
373 assert_equal 'This is the test_new issue', issue.subject
374 end
374 end
375
375
376 def test_post_create
376 def test_post_create
377 @request.session[:user_id] = 2
377 @request.session[:user_id] = 2
378 assert_difference 'Issue.count' do
378 assert_difference 'Issue.count' do
379 post :create, :project_id => 1,
379 post :create, :project_id => 1,
380 :issue => {:tracker_id => 3,
380 :issue => {:tracker_id => 3,
381 :status_id => 2,
381 :status_id => 2,
382 :subject => 'This is the test_new issue',
382 :subject => 'This is the test_new issue',
383 :description => 'This is the description',
383 :description => 'This is the description',
384 :priority_id => 5,
384 :priority_id => 5,
385 :start_date => '2010-11-07',
385 :start_date => '2010-11-07',
386 :estimated_hours => '',
386 :estimated_hours => '',
387 :custom_field_values => {'2' => 'Value for field 2'}}
387 :custom_field_values => {'2' => 'Value for field 2'}}
388 end
388 end
389 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
389 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
390
390
391 issue = Issue.find_by_subject('This is the test_new issue')
391 issue = Issue.find_by_subject('This is the test_new issue')
392 assert_not_nil issue
392 assert_not_nil issue
393 assert_equal 2, issue.author_id
393 assert_equal 2, issue.author_id
394 assert_equal 3, issue.tracker_id
394 assert_equal 3, issue.tracker_id
395 assert_equal 2, issue.status_id
395 assert_equal 2, issue.status_id
396 assert_equal Date.parse('2010-11-07'), issue.start_date
396 assert_equal Date.parse('2010-11-07'), issue.start_date
397 assert_nil issue.estimated_hours
397 assert_nil issue.estimated_hours
398 v = issue.custom_values.find(:first, :conditions => {:custom_field_id => 2})
398 v = issue.custom_values.find(:first, :conditions => {:custom_field_id => 2})
399 assert_not_nil v
399 assert_not_nil v
400 assert_equal 'Value for field 2', v.value
400 assert_equal 'Value for field 2', v.value
401 end
401 end
402
402
403 def test_post_create_without_start_date
403 def test_post_create_without_start_date
404 @request.session[:user_id] = 2
404 @request.session[:user_id] = 2
405 assert_difference 'Issue.count' do
405 assert_difference 'Issue.count' do
406 post :create, :project_id => 1,
406 post :create, :project_id => 1,
407 :issue => {:tracker_id => 3,
407 :issue => {:tracker_id => 3,
408 :status_id => 2,
408 :status_id => 2,
409 :subject => 'This is the test_new issue',
409 :subject => 'This is the test_new issue',
410 :description => 'This is the description',
410 :description => 'This is the description',
411 :priority_id => 5,
411 :priority_id => 5,
412 :start_date => '',
412 :start_date => '',
413 :estimated_hours => '',
413 :estimated_hours => '',
414 :custom_field_values => {'2' => 'Value for field 2'}}
414 :custom_field_values => {'2' => 'Value for field 2'}}
415 end
415 end
416 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
416 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
417
417
418 issue = Issue.find_by_subject('This is the test_new issue')
418 issue = Issue.find_by_subject('This is the test_new issue')
419 assert_not_nil issue
419 assert_not_nil issue
420 assert_nil issue.start_date
420 assert_nil issue.start_date
421 end
421 end
422
422
423 def test_post_create_and_continue
423 def test_post_create_and_continue
424 @request.session[:user_id] = 2
424 @request.session[:user_id] = 2
425 post :create, :project_id => 1,
425 post :create, :project_id => 1,
426 :issue => {:tracker_id => 3,
426 :issue => {:tracker_id => 3,
427 :subject => 'This is first issue',
427 :subject => 'This is first issue',
428 :priority_id => 5},
428 :priority_id => 5},
429 :continue => ''
429 :continue => ''
430 assert_redirected_to :controller => 'issues', :action => 'new', :project_id => 'ecookbook',
430 assert_redirected_to :controller => 'issues', :action => 'new', :project_id => 'ecookbook',
431 :issue => {:tracker_id => 3}
431 :issue => {:tracker_id => 3}
432 end
432 end
433
433
434 def test_post_create_without_custom_fields_param
434 def test_post_create_without_custom_fields_param
435 @request.session[:user_id] = 2
435 @request.session[:user_id] = 2
436 assert_difference 'Issue.count' do
436 assert_difference 'Issue.count' do
437 post :create, :project_id => 1,
437 post :create, :project_id => 1,
438 :issue => {:tracker_id => 1,
438 :issue => {:tracker_id => 1,
439 :subject => 'This is the test_new issue',
439 :subject => 'This is the test_new issue',
440 :description => 'This is the description',
440 :description => 'This is the description',
441 :priority_id => 5}
441 :priority_id => 5}
442 end
442 end
443 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
443 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
444 end
444 end
445
445
446 def test_post_create_with_required_custom_field_and_without_custom_fields_param
446 def test_post_create_with_required_custom_field_and_without_custom_fields_param
447 field = IssueCustomField.find_by_name('Database')
447 field = IssueCustomField.find_by_name('Database')
448 field.update_attribute(:is_required, true)
448 field.update_attribute(:is_required, true)
449
449
450 @request.session[:user_id] = 2
450 @request.session[:user_id] = 2
451 post :create, :project_id => 1,
451 post :create, :project_id => 1,
452 :issue => {:tracker_id => 1,
452 :issue => {:tracker_id => 1,
453 :subject => 'This is the test_new issue',
453 :subject => 'This is the test_new issue',
454 :description => 'This is the description',
454 :description => 'This is the description',
455 :priority_id => 5}
455 :priority_id => 5}
456 assert_response :success
456 assert_response :success
457 assert_template 'new'
457 assert_template 'new'
458 issue = assigns(:issue)
458 issue = assigns(:issue)
459 assert_not_nil issue
459 assert_not_nil issue
460 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
460 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
461 end
461 end
462
462
463 def test_post_create_with_watchers
463 def test_post_create_with_watchers
464 @request.session[:user_id] = 2
464 @request.session[:user_id] = 2
465 ActionMailer::Base.deliveries.clear
465 ActionMailer::Base.deliveries.clear
466
466
467 assert_difference 'Watcher.count', 2 do
467 assert_difference 'Watcher.count', 2 do
468 post :create, :project_id => 1,
468 post :create, :project_id => 1,
469 :issue => {:tracker_id => 1,
469 :issue => {:tracker_id => 1,
470 :subject => 'This is a new issue with watchers',
470 :subject => 'This is a new issue with watchers',
471 :description => 'This is the description',
471 :description => 'This is the description',
472 :priority_id => 5,
472 :priority_id => 5,
473 :watcher_user_ids => ['2', '3']}
473 :watcher_user_ids => ['2', '3']}
474 end
474 end
475 issue = Issue.find_by_subject('This is a new issue with watchers')
475 issue = Issue.find_by_subject('This is a new issue with watchers')
476 assert_not_nil issue
476 assert_not_nil issue
477 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
477 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
478
478
479 # Watchers added
479 # Watchers added
480 assert_equal [2, 3], issue.watcher_user_ids.sort
480 assert_equal [2, 3], issue.watcher_user_ids.sort
481 assert issue.watched_by?(User.find(3))
481 assert issue.watched_by?(User.find(3))
482 # Watchers notified
482 # Watchers notified
483 mail = ActionMailer::Base.deliveries.last
483 mail = ActionMailer::Base.deliveries.last
484 assert_kind_of TMail::Mail, mail
484 assert_kind_of TMail::Mail, mail
485 assert [mail.bcc, mail.cc].flatten.include?(User.find(3).mail)
485 assert [mail.bcc, mail.cc].flatten.include?(User.find(3).mail)
486 end
486 end
487
487
488 def test_post_create_subissue
488 def test_post_create_subissue
489 @request.session[:user_id] = 2
489 @request.session[:user_id] = 2
490
490
491 assert_difference 'Issue.count' do
491 assert_difference 'Issue.count' do
492 post :create, :project_id => 1,
492 post :create, :project_id => 1,
493 :issue => {:tracker_id => 1,
493 :issue => {:tracker_id => 1,
494 :subject => 'This is a child issue',
494 :subject => 'This is a child issue',
495 :parent_issue_id => 2}
495 :parent_issue_id => 2}
496 end
496 end
497 issue = Issue.find_by_subject('This is a child issue')
497 issue = Issue.find_by_subject('This is a child issue')
498 assert_not_nil issue
498 assert_not_nil issue
499 assert_equal Issue.find(2), issue.parent
499 assert_equal Issue.find(2), issue.parent
500 end
500 end
501
502 def test_post_create_subissue_with_non_numeric_parent_id
503 @request.session[:user_id] = 2
504
505 assert_difference 'Issue.count' do
506 post :create, :project_id => 1,
507 :issue => {:tracker_id => 1,
508 :subject => 'This is a child issue',
509 :parent_issue_id => 'ABC'}
510 end
511 issue = Issue.find_by_subject('This is a child issue')
512 assert_not_nil issue
513 assert_nil issue.parent
514 end
501
515
502 def test_post_create_should_send_a_notification
516 def test_post_create_should_send_a_notification
503 ActionMailer::Base.deliveries.clear
517 ActionMailer::Base.deliveries.clear
504 @request.session[:user_id] = 2
518 @request.session[:user_id] = 2
505 assert_difference 'Issue.count' do
519 assert_difference 'Issue.count' do
506 post :create, :project_id => 1,
520 post :create, :project_id => 1,
507 :issue => {:tracker_id => 3,
521 :issue => {:tracker_id => 3,
508 :subject => 'This is the test_new issue',
522 :subject => 'This is the test_new issue',
509 :description => 'This is the description',
523 :description => 'This is the description',
510 :priority_id => 5,
524 :priority_id => 5,
511 :estimated_hours => '',
525 :estimated_hours => '',
512 :custom_field_values => {'2' => 'Value for field 2'}}
526 :custom_field_values => {'2' => 'Value for field 2'}}
513 end
527 end
514 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
528 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
515
529
516 assert_equal 1, ActionMailer::Base.deliveries.size
530 assert_equal 1, ActionMailer::Base.deliveries.size
517 end
531 end
518
532
519 def test_post_create_should_preserve_fields_values_on_validation_failure
533 def test_post_create_should_preserve_fields_values_on_validation_failure
520 @request.session[:user_id] = 2
534 @request.session[:user_id] = 2
521 post :create, :project_id => 1,
535 post :create, :project_id => 1,
522 :issue => {:tracker_id => 1,
536 :issue => {:tracker_id => 1,
523 # empty subject
537 # empty subject
524 :subject => '',
538 :subject => '',
525 :description => 'This is a description',
539 :description => 'This is a description',
526 :priority_id => 6,
540 :priority_id => 6,
527 :custom_field_values => {'1' => 'Oracle', '2' => 'Value for field 2'}}
541 :custom_field_values => {'1' => 'Oracle', '2' => 'Value for field 2'}}
528 assert_response :success
542 assert_response :success
529 assert_template 'new'
543 assert_template 'new'
530
544
531 assert_tag :textarea, :attributes => { :name => 'issue[description]' },
545 assert_tag :textarea, :attributes => { :name => 'issue[description]' },
532 :content => 'This is a description'
546 :content => 'This is a description'
533 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
547 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
534 :child => { :tag => 'option', :attributes => { :selected => 'selected',
548 :child => { :tag => 'option', :attributes => { :selected => 'selected',
535 :value => '6' },
549 :value => '6' },
536 :content => 'High' }
550 :content => 'High' }
537 # Custom fields
551 # Custom fields
538 assert_tag :select, :attributes => { :name => 'issue[custom_field_values][1]' },
552 assert_tag :select, :attributes => { :name => 'issue[custom_field_values][1]' },
539 :child => { :tag => 'option', :attributes => { :selected => 'selected',
553 :child => { :tag => 'option', :attributes => { :selected => 'selected',
540 :value => 'Oracle' },
554 :value => 'Oracle' },
541 :content => 'Oracle' }
555 :content => 'Oracle' }
542 assert_tag :input, :attributes => { :name => 'issue[custom_field_values][2]',
556 assert_tag :input, :attributes => { :name => 'issue[custom_field_values][2]',
543 :value => 'Value for field 2'}
557 :value => 'Value for field 2'}
544 end
558 end
545
559
546 def test_post_create_should_ignore_non_safe_attributes
560 def test_post_create_should_ignore_non_safe_attributes
547 @request.session[:user_id] = 2
561 @request.session[:user_id] = 2
548 assert_nothing_raised do
562 assert_nothing_raised do
549 post :create, :project_id => 1, :issue => { :tracker => "A param can not be a Tracker" }
563 post :create, :project_id => 1, :issue => { :tracker => "A param can not be a Tracker" }
550 end
564 end
551 end
565 end
552
566
553 context "without workflow privilege" do
567 context "without workflow privilege" do
554 setup do
568 setup do
555 Workflow.delete_all(["role_id = ?", Role.anonymous.id])
569 Workflow.delete_all(["role_id = ?", Role.anonymous.id])
556 Role.anonymous.add_permission! :add_issues
570 Role.anonymous.add_permission! :add_issues
557 end
571 end
558
572
559 context "#new" do
573 context "#new" do
560 should "propose default status only" do
574 should "propose default status only" do
561 get :new, :project_id => 1
575 get :new, :project_id => 1
562 assert_response :success
576 assert_response :success
563 assert_template 'new'
577 assert_template 'new'
564 assert_tag :tag => 'select',
578 assert_tag :tag => 'select',
565 :attributes => {:name => 'issue[status_id]'},
579 :attributes => {:name => 'issue[status_id]'},
566 :children => {:count => 1},
580 :children => {:count => 1},
567 :child => {:tag => 'option', :attributes => {:value => IssueStatus.default.id.to_s}}
581 :child => {:tag => 'option', :attributes => {:value => IssueStatus.default.id.to_s}}
568 end
582 end
569
583
570 should "accept default status" do
584 should "accept default status" do
571 assert_difference 'Issue.count' do
585 assert_difference 'Issue.count' do
572 post :create, :project_id => 1,
586 post :create, :project_id => 1,
573 :issue => {:tracker_id => 1,
587 :issue => {:tracker_id => 1,
574 :subject => 'This is an issue',
588 :subject => 'This is an issue',
575 :status_id => 1}
589 :status_id => 1}
576 end
590 end
577 issue = Issue.last(:order => 'id')
591 issue = Issue.last(:order => 'id')
578 assert_equal IssueStatus.default, issue.status
592 assert_equal IssueStatus.default, issue.status
579 end
593 end
580
594
581 should "ignore unauthorized status" do
595 should "ignore unauthorized status" do
582 assert_difference 'Issue.count' do
596 assert_difference 'Issue.count' do
583 post :create, :project_id => 1,
597 post :create, :project_id => 1,
584 :issue => {:tracker_id => 1,
598 :issue => {:tracker_id => 1,
585 :subject => 'This is an issue',
599 :subject => 'This is an issue',
586 :status_id => 3}
600 :status_id => 3}
587 end
601 end
588 issue = Issue.last(:order => 'id')
602 issue = Issue.last(:order => 'id')
589 assert_equal IssueStatus.default, issue.status
603 assert_equal IssueStatus.default, issue.status
590 end
604 end
591 end
605 end
592 end
606 end
593
607
594 def test_copy_issue
608 def test_copy_issue
595 @request.session[:user_id] = 2
609 @request.session[:user_id] = 2
596 get :new, :project_id => 1, :copy_from => 1
610 get :new, :project_id => 1, :copy_from => 1
597 assert_template 'new'
611 assert_template 'new'
598 assert_not_nil assigns(:issue)
612 assert_not_nil assigns(:issue)
599 orig = Issue.find(1)
613 orig = Issue.find(1)
600 assert_equal orig.subject, assigns(:issue).subject
614 assert_equal orig.subject, assigns(:issue).subject
601 end
615 end
602
616
603 def test_get_edit
617 def test_get_edit
604 @request.session[:user_id] = 2
618 @request.session[:user_id] = 2
605 get :edit, :id => 1
619 get :edit, :id => 1
606 assert_response :success
620 assert_response :success
607 assert_template 'edit'
621 assert_template 'edit'
608 assert_not_nil assigns(:issue)
622 assert_not_nil assigns(:issue)
609 assert_equal Issue.find(1), assigns(:issue)
623 assert_equal Issue.find(1), assigns(:issue)
610 end
624 end
611
625
612 def test_get_edit_with_params
626 def test_get_edit_with_params
613 @request.session[:user_id] = 2
627 @request.session[:user_id] = 2
614 get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 }
628 get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 }
615 assert_response :success
629 assert_response :success
616 assert_template 'edit'
630 assert_template 'edit'
617
631
618 issue = assigns(:issue)
632 issue = assigns(:issue)
619 assert_not_nil issue
633 assert_not_nil issue
620
634
621 assert_equal 5, issue.status_id
635 assert_equal 5, issue.status_id
622 assert_tag :select, :attributes => { :name => 'issue[status_id]' },
636 assert_tag :select, :attributes => { :name => 'issue[status_id]' },
623 :child => { :tag => 'option',
637 :child => { :tag => 'option',
624 :content => 'Closed',
638 :content => 'Closed',
625 :attributes => { :selected => 'selected' } }
639 :attributes => { :selected => 'selected' } }
626
640
627 assert_equal 7, issue.priority_id
641 assert_equal 7, issue.priority_id
628 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
642 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
629 :child => { :tag => 'option',
643 :child => { :tag => 'option',
630 :content => 'Urgent',
644 :content => 'Urgent',
631 :attributes => { :selected => 'selected' } }
645 :attributes => { :selected => 'selected' } }
632 end
646 end
633
647
634 def test_update_edit_form
648 def test_update_edit_form
635 @request.session[:user_id] = 2
649 @request.session[:user_id] = 2
636 xhr :post, :new, :project_id => 1,
650 xhr :post, :new, :project_id => 1,
637 :id => 1,
651 :id => 1,
638 :issue => {:tracker_id => 2,
652 :issue => {:tracker_id => 2,
639 :subject => 'This is the test_new issue',
653 :subject => 'This is the test_new issue',
640 :description => 'This is the description',
654 :description => 'This is the description',
641 :priority_id => 5}
655 :priority_id => 5}
642 assert_response :success
656 assert_response :success
643 assert_template 'attributes'
657 assert_template 'attributes'
644
658
645 issue = assigns(:issue)
659 issue = assigns(:issue)
646 assert_kind_of Issue, issue
660 assert_kind_of Issue, issue
647 assert_equal 1, issue.id
661 assert_equal 1, issue.id
648 assert_equal 1, issue.project_id
662 assert_equal 1, issue.project_id
649 assert_equal 2, issue.tracker_id
663 assert_equal 2, issue.tracker_id
650 assert_equal 'This is the test_new issue', issue.subject
664 assert_equal 'This is the test_new issue', issue.subject
651 end
665 end
652
666
653 def test_update_using_invalid_http_verbs
667 def test_update_using_invalid_http_verbs
654 @request.session[:user_id] = 2
668 @request.session[:user_id] = 2
655 subject = 'Updated by an invalid http verb'
669 subject = 'Updated by an invalid http verb'
656
670
657 get :update, :id => 1, :issue => {:subject => subject}
671 get :update, :id => 1, :issue => {:subject => subject}
658 assert_not_equal subject, Issue.find(1).subject
672 assert_not_equal subject, Issue.find(1).subject
659
673
660 post :update, :id => 1, :issue => {:subject => subject}
674 post :update, :id => 1, :issue => {:subject => subject}
661 assert_not_equal subject, Issue.find(1).subject
675 assert_not_equal subject, Issue.find(1).subject
662
676
663 delete :update, :id => 1, :issue => {:subject => subject}
677 delete :update, :id => 1, :issue => {:subject => subject}
664 assert_not_equal subject, Issue.find(1).subject
678 assert_not_equal subject, Issue.find(1).subject
665 end
679 end
666
680
667 def test_put_update_without_custom_fields_param
681 def test_put_update_without_custom_fields_param
668 @request.session[:user_id] = 2
682 @request.session[:user_id] = 2
669 ActionMailer::Base.deliveries.clear
683 ActionMailer::Base.deliveries.clear
670
684
671 issue = Issue.find(1)
685 issue = Issue.find(1)
672 assert_equal '125', issue.custom_value_for(2).value
686 assert_equal '125', issue.custom_value_for(2).value
673 old_subject = issue.subject
687 old_subject = issue.subject
674 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
688 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
675
689
676 assert_difference('Journal.count') do
690 assert_difference('Journal.count') do
677 assert_difference('JournalDetail.count', 2) do
691 assert_difference('JournalDetail.count', 2) do
678 put :update, :id => 1, :issue => {:subject => new_subject,
692 put :update, :id => 1, :issue => {:subject => new_subject,
679 :priority_id => '6',
693 :priority_id => '6',
680 :category_id => '1' # no change
694 :category_id => '1' # no change
681 }
695 }
682 end
696 end
683 end
697 end
684 assert_redirected_to :action => 'show', :id => '1'
698 assert_redirected_to :action => 'show', :id => '1'
685 issue.reload
699 issue.reload
686 assert_equal new_subject, issue.subject
700 assert_equal new_subject, issue.subject
687 # Make sure custom fields were not cleared
701 # Make sure custom fields were not cleared
688 assert_equal '125', issue.custom_value_for(2).value
702 assert_equal '125', issue.custom_value_for(2).value
689
703
690 mail = ActionMailer::Base.deliveries.last
704 mail = ActionMailer::Base.deliveries.last
691 assert_kind_of TMail::Mail, mail
705 assert_kind_of TMail::Mail, mail
692 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
706 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
693 assert mail.body.include?("Subject changed from #{old_subject} to #{new_subject}")
707 assert mail.body.include?("Subject changed from #{old_subject} to #{new_subject}")
694 end
708 end
695
709
696 def test_put_update_with_custom_field_change
710 def test_put_update_with_custom_field_change
697 @request.session[:user_id] = 2
711 @request.session[:user_id] = 2
698 issue = Issue.find(1)
712 issue = Issue.find(1)
699 assert_equal '125', issue.custom_value_for(2).value
713 assert_equal '125', issue.custom_value_for(2).value
700
714
701 assert_difference('Journal.count') do
715 assert_difference('Journal.count') do
702 assert_difference('JournalDetail.count', 3) do
716 assert_difference('JournalDetail.count', 3) do
703 put :update, :id => 1, :issue => {:subject => 'Custom field change',
717 put :update, :id => 1, :issue => {:subject => 'Custom field change',
704 :priority_id => '6',
718 :priority_id => '6',
705 :category_id => '1', # no change
719 :category_id => '1', # no change
706 :custom_field_values => { '2' => 'New custom value' }
720 :custom_field_values => { '2' => 'New custom value' }
707 }
721 }
708 end
722 end
709 end
723 end
710 assert_redirected_to :action => 'show', :id => '1'
724 assert_redirected_to :action => 'show', :id => '1'
711 issue.reload
725 issue.reload
712 assert_equal 'New custom value', issue.custom_value_for(2).value
726 assert_equal 'New custom value', issue.custom_value_for(2).value
713
727
714 mail = ActionMailer::Base.deliveries.last
728 mail = ActionMailer::Base.deliveries.last
715 assert_kind_of TMail::Mail, mail
729 assert_kind_of TMail::Mail, mail
716 assert mail.body.include?("Searchable field changed from 125 to New custom value")
730 assert mail.body.include?("Searchable field changed from 125 to New custom value")
717 end
731 end
718
732
719 def test_put_update_with_status_and_assignee_change
733 def test_put_update_with_status_and_assignee_change
720 issue = Issue.find(1)
734 issue = Issue.find(1)
721 assert_equal 1, issue.status_id
735 assert_equal 1, issue.status_id
722 @request.session[:user_id] = 2
736 @request.session[:user_id] = 2
723 assert_difference('TimeEntry.count', 0) do
737 assert_difference('TimeEntry.count', 0) do
724 put :update,
738 put :update,
725 :id => 1,
739 :id => 1,
726 :issue => { :status_id => 2, :assigned_to_id => 3 },
740 :issue => { :status_id => 2, :assigned_to_id => 3 },
727 :notes => 'Assigned to dlopper',
741 :notes => 'Assigned to dlopper',
728 :time_entry => { :hours => '', :comments => '', :activity_id => TimeEntryActivity.first }
742 :time_entry => { :hours => '', :comments => '', :activity_id => TimeEntryActivity.first }
729 end
743 end
730 assert_redirected_to :action => 'show', :id => '1'
744 assert_redirected_to :action => 'show', :id => '1'
731 issue.reload
745 issue.reload
732 assert_equal 2, issue.status_id
746 assert_equal 2, issue.status_id
733 j = Journal.find(:first, :order => 'id DESC')
747 j = Journal.find(:first, :order => 'id DESC')
734 assert_equal 'Assigned to dlopper', j.notes
748 assert_equal 'Assigned to dlopper', j.notes
735 assert_equal 2, j.details.size
749 assert_equal 2, j.details.size
736
750
737 mail = ActionMailer::Base.deliveries.last
751 mail = ActionMailer::Base.deliveries.last
738 assert mail.body.include?("Status changed from New to Assigned")
752 assert mail.body.include?("Status changed from New to Assigned")
739 # subject should contain the new status
753 # subject should contain the new status
740 assert mail.subject.include?("(#{ IssueStatus.find(2).name })")
754 assert mail.subject.include?("(#{ IssueStatus.find(2).name })")
741 end
755 end
742
756
743 def test_put_update_with_note_only
757 def test_put_update_with_note_only
744 notes = 'Note added by IssuesControllerTest#test_update_with_note_only'
758 notes = 'Note added by IssuesControllerTest#test_update_with_note_only'
745 # anonymous user
759 # anonymous user
746 put :update,
760 put :update,
747 :id => 1,
761 :id => 1,
748 :notes => notes
762 :notes => notes
749 assert_redirected_to :action => 'show', :id => '1'
763 assert_redirected_to :action => 'show', :id => '1'
750 j = Journal.find(:first, :order => 'id DESC')
764 j = Journal.find(:first, :order => 'id DESC')
751 assert_equal notes, j.notes
765 assert_equal notes, j.notes
752 assert_equal 0, j.details.size
766 assert_equal 0, j.details.size
753 assert_equal User.anonymous, j.user
767 assert_equal User.anonymous, j.user
754
768
755 mail = ActionMailer::Base.deliveries.last
769 mail = ActionMailer::Base.deliveries.last
756 assert mail.body.include?(notes)
770 assert mail.body.include?(notes)
757 end
771 end
758
772
759 def test_put_update_with_note_and_spent_time
773 def test_put_update_with_note_and_spent_time
760 @request.session[:user_id] = 2
774 @request.session[:user_id] = 2
761 spent_hours_before = Issue.find(1).spent_hours
775 spent_hours_before = Issue.find(1).spent_hours
762 assert_difference('TimeEntry.count') do
776 assert_difference('TimeEntry.count') do
763 put :update,
777 put :update,
764 :id => 1,
778 :id => 1,
765 :notes => '2.5 hours added',
779 :notes => '2.5 hours added',
766 :time_entry => { :hours => '2.5', :comments => 'test_put_update_with_note_and_spent_time', :activity_id => TimeEntryActivity.first.id }
780 :time_entry => { :hours => '2.5', :comments => 'test_put_update_with_note_and_spent_time', :activity_id => TimeEntryActivity.first.id }
767 end
781 end
768 assert_redirected_to :action => 'show', :id => '1'
782 assert_redirected_to :action => 'show', :id => '1'
769
783
770 issue = Issue.find(1)
784 issue = Issue.find(1)
771
785
772 j = Journal.find(:first, :order => 'id DESC')
786 j = Journal.find(:first, :order => 'id DESC')
773 assert_equal '2.5 hours added', j.notes
787 assert_equal '2.5 hours added', j.notes
774 assert_equal 0, j.details.size
788 assert_equal 0, j.details.size
775
789
776 t = issue.time_entries.find_by_comments('test_put_update_with_note_and_spent_time')
790 t = issue.time_entries.find_by_comments('test_put_update_with_note_and_spent_time')
777 assert_not_nil t
791 assert_not_nil t
778 assert_equal 2.5, t.hours
792 assert_equal 2.5, t.hours
779 assert_equal spent_hours_before + 2.5, issue.spent_hours
793 assert_equal spent_hours_before + 2.5, issue.spent_hours
780 end
794 end
781
795
782 def test_put_update_with_attachment_only
796 def test_put_update_with_attachment_only
783 set_tmp_attachments_directory
797 set_tmp_attachments_directory
784
798
785 # Delete all fixtured journals, a race condition can occur causing the wrong
799 # Delete all fixtured journals, a race condition can occur causing the wrong
786 # journal to get fetched in the next find.
800 # journal to get fetched in the next find.
787 Journal.delete_all
801 Journal.delete_all
788
802
789 # anonymous user
803 # anonymous user
790 put :update,
804 put :update,
791 :id => 1,
805 :id => 1,
792 :notes => '',
806 :notes => '',
793 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
807 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
794 assert_redirected_to :action => 'show', :id => '1'
808 assert_redirected_to :action => 'show', :id => '1'
795 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
809 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
796 assert j.notes.blank?
810 assert j.notes.blank?
797 assert_equal 1, j.details.size
811 assert_equal 1, j.details.size
798 assert_equal 'testfile.txt', j.details.first.value
812 assert_equal 'testfile.txt', j.details.first.value
799 assert_equal User.anonymous, j.user
813 assert_equal User.anonymous, j.user
800
814
801 mail = ActionMailer::Base.deliveries.last
815 mail = ActionMailer::Base.deliveries.last
802 assert mail.body.include?('testfile.txt')
816 assert mail.body.include?('testfile.txt')
803 end
817 end
804
818
805 def test_put_update_with_attachment_that_fails_to_save
819 def test_put_update_with_attachment_that_fails_to_save
806 set_tmp_attachments_directory
820 set_tmp_attachments_directory
807
821
808 # Delete all fixtured journals, a race condition can occur causing the wrong
822 # Delete all fixtured journals, a race condition can occur causing the wrong
809 # journal to get fetched in the next find.
823 # journal to get fetched in the next find.
810 Journal.delete_all
824 Journal.delete_all
811
825
812 # Mock out the unsaved attachment
826 # Mock out the unsaved attachment
813 Attachment.any_instance.stubs(:create).returns(Attachment.new)
827 Attachment.any_instance.stubs(:create).returns(Attachment.new)
814
828
815 # anonymous user
829 # anonymous user
816 put :update,
830 put :update,
817 :id => 1,
831 :id => 1,
818 :notes => '',
832 :notes => '',
819 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
833 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
820 assert_redirected_to :action => 'show', :id => '1'
834 assert_redirected_to :action => 'show', :id => '1'
821 assert_equal '1 file(s) could not be saved.', flash[:warning]
835 assert_equal '1 file(s) could not be saved.', flash[:warning]
822
836
823 end if Object.const_defined?(:Mocha)
837 end if Object.const_defined?(:Mocha)
824
838
825 def test_put_update_with_no_change
839 def test_put_update_with_no_change
826 issue = Issue.find(1)
840 issue = Issue.find(1)
827 issue.journals.clear
841 issue.journals.clear
828 ActionMailer::Base.deliveries.clear
842 ActionMailer::Base.deliveries.clear
829
843
830 put :update,
844 put :update,
831 :id => 1,
845 :id => 1,
832 :notes => ''
846 :notes => ''
833 assert_redirected_to :action => 'show', :id => '1'
847 assert_redirected_to :action => 'show', :id => '1'
834
848
835 issue.reload
849 issue.reload
836 assert issue.journals.empty?
850 assert issue.journals.empty?
837 # No email should be sent
851 # No email should be sent
838 assert ActionMailer::Base.deliveries.empty?
852 assert ActionMailer::Base.deliveries.empty?
839 end
853 end
840
854
841 def test_put_update_should_send_a_notification
855 def test_put_update_should_send_a_notification
842 @request.session[:user_id] = 2
856 @request.session[:user_id] = 2
843 ActionMailer::Base.deliveries.clear
857 ActionMailer::Base.deliveries.clear
844 issue = Issue.find(1)
858 issue = Issue.find(1)
845 old_subject = issue.subject
859 old_subject = issue.subject
846 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
860 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
847
861
848 put :update, :id => 1, :issue => {:subject => new_subject,
862 put :update, :id => 1, :issue => {:subject => new_subject,
849 :priority_id => '6',
863 :priority_id => '6',
850 :category_id => '1' # no change
864 :category_id => '1' # no change
851 }
865 }
852 assert_equal 1, ActionMailer::Base.deliveries.size
866 assert_equal 1, ActionMailer::Base.deliveries.size
853 end
867 end
854
868
855 def test_put_update_with_invalid_spent_time
869 def test_put_update_with_invalid_spent_time
856 @request.session[:user_id] = 2
870 @request.session[:user_id] = 2
857 notes = 'Note added by IssuesControllerTest#test_post_edit_with_invalid_spent_time'
871 notes = 'Note added by IssuesControllerTest#test_post_edit_with_invalid_spent_time'
858
872
859 assert_no_difference('Journal.count') do
873 assert_no_difference('Journal.count') do
860 put :update,
874 put :update,
861 :id => 1,
875 :id => 1,
862 :notes => notes,
876 :notes => notes,
863 :time_entry => {"comments"=>"", "activity_id"=>"", "hours"=>"2z"}
877 :time_entry => {"comments"=>"", "activity_id"=>"", "hours"=>"2z"}
864 end
878 end
865 assert_response :success
879 assert_response :success
866 assert_template 'edit'
880 assert_template 'edit'
867
881
868 assert_tag :textarea, :attributes => { :name => 'notes' },
882 assert_tag :textarea, :attributes => { :name => 'notes' },
869 :content => notes
883 :content => notes
870 assert_tag :input, :attributes => { :name => 'time_entry[hours]', :value => "2z" }
884 assert_tag :input, :attributes => { :name => 'time_entry[hours]', :value => "2z" }
871 end
885 end
872
886
873 def test_put_update_should_allow_fixed_version_to_be_set_to_a_subproject
887 def test_put_update_should_allow_fixed_version_to_be_set_to_a_subproject
874 issue = Issue.find(2)
888 issue = Issue.find(2)
875 @request.session[:user_id] = 2
889 @request.session[:user_id] = 2
876
890
877 put :update,
891 put :update,
878 :id => issue.id,
892 :id => issue.id,
879 :issue => {
893 :issue => {
880 :fixed_version_id => 4
894 :fixed_version_id => 4
881 }
895 }
882
896
883 assert_response :redirect
897 assert_response :redirect
884 issue.reload
898 issue.reload
885 assert_equal 4, issue.fixed_version_id
899 assert_equal 4, issue.fixed_version_id
886 assert_not_equal issue.project_id, issue.fixed_version.project_id
900 assert_not_equal issue.project_id, issue.fixed_version.project_id
887 end
901 end
888
902
889 def test_put_update_should_redirect_back_using_the_back_url_parameter
903 def test_put_update_should_redirect_back_using_the_back_url_parameter
890 issue = Issue.find(2)
904 issue = Issue.find(2)
891 @request.session[:user_id] = 2
905 @request.session[:user_id] = 2
892
906
893 put :update,
907 put :update,
894 :id => issue.id,
908 :id => issue.id,
895 :issue => {
909 :issue => {
896 :fixed_version_id => 4
910 :fixed_version_id => 4
897 },
911 },
898 :back_url => '/issues'
912 :back_url => '/issues'
899
913
900 assert_response :redirect
914 assert_response :redirect
901 assert_redirected_to '/issues'
915 assert_redirected_to '/issues'
902 end
916 end
903
917
904 def test_put_update_should_not_redirect_back_using_the_back_url_parameter_off_the_host
918 def test_put_update_should_not_redirect_back_using_the_back_url_parameter_off_the_host
905 issue = Issue.find(2)
919 issue = Issue.find(2)
906 @request.session[:user_id] = 2
920 @request.session[:user_id] = 2
907
921
908 put :update,
922 put :update,
909 :id => issue.id,
923 :id => issue.id,
910 :issue => {
924 :issue => {
911 :fixed_version_id => 4
925 :fixed_version_id => 4
912 },
926 },
913 :back_url => 'http://google.com'
927 :back_url => 'http://google.com'
914
928
915 assert_response :redirect
929 assert_response :redirect
916 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue.id
930 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue.id
917 end
931 end
918
932
919 def test_get_bulk_edit
933 def test_get_bulk_edit
920 @request.session[:user_id] = 2
934 @request.session[:user_id] = 2
921 get :bulk_edit, :ids => [1, 2]
935 get :bulk_edit, :ids => [1, 2]
922 assert_response :success
936 assert_response :success
923 assert_template 'bulk_edit'
937 assert_template 'bulk_edit'
924
938
925 # Project specific custom field, date type
939 # Project specific custom field, date type
926 field = CustomField.find(9)
940 field = CustomField.find(9)
927 assert !field.is_for_all?
941 assert !field.is_for_all?
928 assert_equal 'date', field.field_format
942 assert_equal 'date', field.field_format
929 assert_tag :input, :attributes => {:name => 'issue[custom_field_values][9]'}
943 assert_tag :input, :attributes => {:name => 'issue[custom_field_values][9]'}
930
944
931 # System wide custom field
945 # System wide custom field
932 assert CustomField.find(1).is_for_all?
946 assert CustomField.find(1).is_for_all?
933 assert_tag :select, :attributes => {:name => 'issue[custom_field_values][1]'}
947 assert_tag :select, :attributes => {:name => 'issue[custom_field_values][1]'}
934 end
948 end
935
949
936 def test_bulk_update
950 def test_bulk_update
937 @request.session[:user_id] = 2
951 @request.session[:user_id] = 2
938 # update issues priority
952 # update issues priority
939 post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing',
953 post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing',
940 :issue => {:priority_id => 7,
954 :issue => {:priority_id => 7,
941 :assigned_to_id => '',
955 :assigned_to_id => '',
942 :custom_field_values => {'2' => ''}}
956 :custom_field_values => {'2' => ''}}
943
957
944 assert_response 302
958 assert_response 302
945 # check that the issues were updated
959 # check that the issues were updated
946 assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id}
960 assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id}
947
961
948 issue = Issue.find(1)
962 issue = Issue.find(1)
949 journal = issue.journals.find(:first, :order => 'created_on DESC')
963 journal = issue.journals.find(:first, :order => 'created_on DESC')
950 assert_equal '125', issue.custom_value_for(2).value
964 assert_equal '125', issue.custom_value_for(2).value
951 assert_equal 'Bulk editing', journal.notes
965 assert_equal 'Bulk editing', journal.notes
952 assert_equal 1, journal.details.size
966 assert_equal 1, journal.details.size
953 end
967 end
954
968
955 def test_bullk_update_should_send_a_notification
969 def test_bullk_update_should_send_a_notification
956 @request.session[:user_id] = 2
970 @request.session[:user_id] = 2
957 ActionMailer::Base.deliveries.clear
971 ActionMailer::Base.deliveries.clear
958 post(:bulk_update,
972 post(:bulk_update,
959 {
973 {
960 :ids => [1, 2],
974 :ids => [1, 2],
961 :notes => 'Bulk editing',
975 :notes => 'Bulk editing',
962 :issue => {
976 :issue => {
963 :priority_id => 7,
977 :priority_id => 7,
964 :assigned_to_id => '',
978 :assigned_to_id => '',
965 :custom_field_values => {'2' => ''}
979 :custom_field_values => {'2' => ''}
966 }
980 }
967 })
981 })
968
982
969 assert_response 302
983 assert_response 302
970 assert_equal 2, ActionMailer::Base.deliveries.size
984 assert_equal 2, ActionMailer::Base.deliveries.size
971 end
985 end
972
986
973 def test_bulk_update_status
987 def test_bulk_update_status
974 @request.session[:user_id] = 2
988 @request.session[:user_id] = 2
975 # update issues priority
989 # update issues priority
976 post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing status',
990 post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing status',
977 :issue => {:priority_id => '',
991 :issue => {:priority_id => '',
978 :assigned_to_id => '',
992 :assigned_to_id => '',
979 :status_id => '5'}
993 :status_id => '5'}
980
994
981 assert_response 302
995 assert_response 302
982 issue = Issue.find(1)
996 issue = Issue.find(1)
983 assert issue.closed?
997 assert issue.closed?
984 end
998 end
985
999
986 def test_bulk_update_custom_field
1000 def test_bulk_update_custom_field
987 @request.session[:user_id] = 2
1001 @request.session[:user_id] = 2
988 # update issues priority
1002 # update issues priority
989 post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing custom field',
1003 post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing custom field',
990 :issue => {:priority_id => '',
1004 :issue => {:priority_id => '',
991 :assigned_to_id => '',
1005 :assigned_to_id => '',
992 :custom_field_values => {'2' => '777'}}
1006 :custom_field_values => {'2' => '777'}}
993
1007
994 assert_response 302
1008 assert_response 302
995
1009
996 issue = Issue.find(1)
1010 issue = Issue.find(1)
997 journal = issue.journals.find(:first, :order => 'created_on DESC')
1011 journal = issue.journals.find(:first, :order => 'created_on DESC')
998 assert_equal '777', issue.custom_value_for(2).value
1012 assert_equal '777', issue.custom_value_for(2).value
999 assert_equal 1, journal.details.size
1013 assert_equal 1, journal.details.size
1000 assert_equal '125', journal.details.first.old_value
1014 assert_equal '125', journal.details.first.old_value
1001 assert_equal '777', journal.details.first.value
1015 assert_equal '777', journal.details.first.value
1002 end
1016 end
1003
1017
1004 def test_bulk_update_unassign
1018 def test_bulk_update_unassign
1005 assert_not_nil Issue.find(2).assigned_to
1019 assert_not_nil Issue.find(2).assigned_to
1006 @request.session[:user_id] = 2
1020 @request.session[:user_id] = 2
1007 # unassign issues
1021 # unassign issues
1008 post :bulk_update, :ids => [1, 2], :notes => 'Bulk unassigning', :issue => {:assigned_to_id => 'none'}
1022 post :bulk_update, :ids => [1, 2], :notes => 'Bulk unassigning', :issue => {:assigned_to_id => 'none'}
1009 assert_response 302
1023 assert_response 302
1010 # check that the issues were updated
1024 # check that the issues were updated
1011 assert_nil Issue.find(2).assigned_to
1025 assert_nil Issue.find(2).assigned_to
1012 end
1026 end
1013
1027
1014 def test_post_bulk_update_should_allow_fixed_version_to_be_set_to_a_subproject
1028 def test_post_bulk_update_should_allow_fixed_version_to_be_set_to_a_subproject
1015 @request.session[:user_id] = 2
1029 @request.session[:user_id] = 2
1016
1030
1017 post :bulk_update, :ids => [1,2], :issue => {:fixed_version_id => 4}
1031 post :bulk_update, :ids => [1,2], :issue => {:fixed_version_id => 4}
1018
1032
1019 assert_response :redirect
1033 assert_response :redirect
1020 issues = Issue.find([1,2])
1034 issues = Issue.find([1,2])
1021 issues.each do |issue|
1035 issues.each do |issue|
1022 assert_equal 4, issue.fixed_version_id
1036 assert_equal 4, issue.fixed_version_id
1023 assert_not_equal issue.project_id, issue.fixed_version.project_id
1037 assert_not_equal issue.project_id, issue.fixed_version.project_id
1024 end
1038 end
1025 end
1039 end
1026
1040
1027 def test_post_bulk_update_should_redirect_back_using_the_back_url_parameter
1041 def test_post_bulk_update_should_redirect_back_using_the_back_url_parameter
1028 @request.session[:user_id] = 2
1042 @request.session[:user_id] = 2
1029 post :bulk_update, :ids => [1,2], :back_url => '/issues'
1043 post :bulk_update, :ids => [1,2], :back_url => '/issues'
1030
1044
1031 assert_response :redirect
1045 assert_response :redirect
1032 assert_redirected_to '/issues'
1046 assert_redirected_to '/issues'
1033 end
1047 end
1034
1048
1035 def test_post_bulk_update_should_not_redirect_back_using_the_back_url_parameter_off_the_host
1049 def test_post_bulk_update_should_not_redirect_back_using_the_back_url_parameter_off_the_host
1036 @request.session[:user_id] = 2
1050 @request.session[:user_id] = 2
1037 post :bulk_update, :ids => [1,2], :back_url => 'http://google.com'
1051 post :bulk_update, :ids => [1,2], :back_url => 'http://google.com'
1038
1052
1039 assert_response :redirect
1053 assert_response :redirect
1040 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => Project.find(1).identifier
1054 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => Project.find(1).identifier
1041 end
1055 end
1042
1056
1043 def test_destroy_issue_with_no_time_entries
1057 def test_destroy_issue_with_no_time_entries
1044 assert_nil TimeEntry.find_by_issue_id(2)
1058 assert_nil TimeEntry.find_by_issue_id(2)
1045 @request.session[:user_id] = 2
1059 @request.session[:user_id] = 2
1046 post :destroy, :id => 2
1060 post :destroy, :id => 2
1047 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1061 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1048 assert_nil Issue.find_by_id(2)
1062 assert_nil Issue.find_by_id(2)
1049 end
1063 end
1050
1064
1051 def test_destroy_issues_with_time_entries
1065 def test_destroy_issues_with_time_entries
1052 @request.session[:user_id] = 2
1066 @request.session[:user_id] = 2
1053 post :destroy, :ids => [1, 3]
1067 post :destroy, :ids => [1, 3]
1054 assert_response :success
1068 assert_response :success
1055 assert_template 'destroy'
1069 assert_template 'destroy'
1056 assert_not_nil assigns(:hours)
1070 assert_not_nil assigns(:hours)
1057 assert Issue.find_by_id(1) && Issue.find_by_id(3)
1071 assert Issue.find_by_id(1) && Issue.find_by_id(3)
1058 end
1072 end
1059
1073
1060 def test_destroy_issues_and_destroy_time_entries
1074 def test_destroy_issues_and_destroy_time_entries
1061 @request.session[:user_id] = 2
1075 @request.session[:user_id] = 2
1062 post :destroy, :ids => [1, 3], :todo => 'destroy'
1076 post :destroy, :ids => [1, 3], :todo => 'destroy'
1063 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1077 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1064 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1078 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1065 assert_nil TimeEntry.find_by_id([1, 2])
1079 assert_nil TimeEntry.find_by_id([1, 2])
1066 end
1080 end
1067
1081
1068 def test_destroy_issues_and_assign_time_entries_to_project
1082 def test_destroy_issues_and_assign_time_entries_to_project
1069 @request.session[:user_id] = 2
1083 @request.session[:user_id] = 2
1070 post :destroy, :ids => [1, 3], :todo => 'nullify'
1084 post :destroy, :ids => [1, 3], :todo => 'nullify'
1071 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1085 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1072 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1086 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1073 assert_nil TimeEntry.find(1).issue_id
1087 assert_nil TimeEntry.find(1).issue_id
1074 assert_nil TimeEntry.find(2).issue_id
1088 assert_nil TimeEntry.find(2).issue_id
1075 end
1089 end
1076
1090
1077 def test_destroy_issues_and_reassign_time_entries_to_another_issue
1091 def test_destroy_issues_and_reassign_time_entries_to_another_issue
1078 @request.session[:user_id] = 2
1092 @request.session[:user_id] = 2
1079 post :destroy, :ids => [1, 3], :todo => 'reassign', :reassign_to_id => 2
1093 post :destroy, :ids => [1, 3], :todo => 'reassign', :reassign_to_id => 2
1080 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1094 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1081 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1095 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1082 assert_equal 2, TimeEntry.find(1).issue_id
1096 assert_equal 2, TimeEntry.find(1).issue_id
1083 assert_equal 2, TimeEntry.find(2).issue_id
1097 assert_equal 2, TimeEntry.find(2).issue_id
1084 end
1098 end
1085
1099
1086 def test_default_search_scope
1100 def test_default_search_scope
1087 get :index
1101 get :index
1088 assert_tag :div, :attributes => {:id => 'quick-search'},
1102 assert_tag :div, :attributes => {:id => 'quick-search'},
1089 :child => {:tag => 'form',
1103 :child => {:tag => 'form',
1090 :child => {:tag => 'input', :attributes => {:name => 'issues', :type => 'hidden', :value => '1'}}}
1104 :child => {:tag => 'input', :attributes => {:name => 'issues', :type => 'hidden', :value => '1'}}}
1091 end
1105 end
1092 end
1106 end
General Comments 0
You need to be logged in to leave comments. Login now