##// END OF EJS Templates
Fixed: ambiguous lft column SQL error on Issue#descendants with a join on projects....
Jean-Philippe Lang -
r5321:949b355ef213
parent child
Show More
@@ -1,963 +1,967
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19
19
20 class IssueTest < ActiveSupport::TestCase
20 class IssueTest < ActiveSupport::TestCase
21 fixtures :projects, :users, :members, :member_roles, :roles,
21 fixtures :projects, :users, :members, :member_roles, :roles,
22 :trackers, :projects_trackers,
22 :trackers, :projects_trackers,
23 :enabled_modules,
23 :enabled_modules,
24 :versions,
24 :versions,
25 :issue_statuses, :issue_categories, :issue_relations, :workflows,
25 :issue_statuses, :issue_categories, :issue_relations, :workflows,
26 :enumerations,
26 :enumerations,
27 :issues,
27 :issues,
28 :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values,
28 :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values,
29 :time_entries
29 :time_entries
30
30
31 def test_create
31 def test_create
32 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'test_create', :description => 'IssueTest#test_create', :estimated_hours => '1:30')
32 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'test_create', :description => 'IssueTest#test_create', :estimated_hours => '1:30')
33 assert issue.save
33 assert issue.save
34 issue.reload
34 issue.reload
35 assert_equal 1.5, issue.estimated_hours
35 assert_equal 1.5, issue.estimated_hours
36 end
36 end
37
37
38 def test_create_minimal
38 def test_create_minimal
39 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'test_create')
39 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'test_create')
40 assert issue.save
40 assert issue.save
41 assert issue.description.nil?
41 assert issue.description.nil?
42 end
42 end
43
43
44 def test_create_with_required_custom_field
44 def test_create_with_required_custom_field
45 field = IssueCustomField.find_by_name('Database')
45 field = IssueCustomField.find_by_name('Database')
46 field.update_attribute(:is_required, true)
46 field.update_attribute(:is_required, true)
47
47
48 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => 'test_create', :description => 'IssueTest#test_create_with_required_custom_field')
48 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => 'test_create', :description => 'IssueTest#test_create_with_required_custom_field')
49 assert issue.available_custom_fields.include?(field)
49 assert issue.available_custom_fields.include?(field)
50 # No value for the custom field
50 # No value for the custom field
51 assert !issue.save
51 assert !issue.save
52 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
52 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
53 # Blank value
53 # Blank value
54 issue.custom_field_values = { field.id => '' }
54 issue.custom_field_values = { field.id => '' }
55 assert !issue.save
55 assert !issue.save
56 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
56 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
57 # Invalid value
57 # Invalid value
58 issue.custom_field_values = { field.id => 'SQLServer' }
58 issue.custom_field_values = { field.id => 'SQLServer' }
59 assert !issue.save
59 assert !issue.save
60 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
60 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
61 # Valid value
61 # Valid value
62 issue.custom_field_values = { field.id => 'PostgreSQL' }
62 issue.custom_field_values = { field.id => 'PostgreSQL' }
63 assert issue.save
63 assert issue.save
64 issue.reload
64 issue.reload
65 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
65 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
66 end
66 end
67
67
68 def assert_visibility_match(user, issues)
68 def assert_visibility_match(user, issues)
69 assert_equal issues.collect(&:id).sort, Issue.all.select {|issue| issue.visible?(user)}.collect(&:id).sort
69 assert_equal issues.collect(&:id).sort, Issue.all.select {|issue| issue.visible?(user)}.collect(&:id).sort
70 end
70 end
71
71
72 def test_visible_scope_for_anonymous
72 def test_visible_scope_for_anonymous
73 # Anonymous user should see issues of public projects only
73 # Anonymous user should see issues of public projects only
74 issues = Issue.visible(User.anonymous).all
74 issues = Issue.visible(User.anonymous).all
75 assert issues.any?
75 assert issues.any?
76 assert_nil issues.detect {|issue| !issue.project.is_public?}
76 assert_nil issues.detect {|issue| !issue.project.is_public?}
77 assert_visibility_match User.anonymous, issues
77 assert_visibility_match User.anonymous, issues
78 end
78 end
79
79
80 def test_visible_scope_for_anonymous_with_own_issues_visibility
80 def test_visible_scope_for_anonymous_with_own_issues_visibility
81 Role.anonymous.update_attribute :issues_visibility, 'own'
81 Role.anonymous.update_attribute :issues_visibility, 'own'
82 Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => User.anonymous.id, :subject => 'Issue by anonymous')
82 Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => User.anonymous.id, :subject => 'Issue by anonymous')
83
83
84 issues = Issue.visible(User.anonymous).all
84 issues = Issue.visible(User.anonymous).all
85 assert issues.any?
85 assert issues.any?
86 assert_nil issues.detect {|issue| issue.author != User.anonymous}
86 assert_nil issues.detect {|issue| issue.author != User.anonymous}
87 assert_visibility_match User.anonymous, issues
87 assert_visibility_match User.anonymous, issues
88 end
88 end
89
89
90 def test_visible_scope_for_anonymous_without_view_issues_permissions
90 def test_visible_scope_for_anonymous_without_view_issues_permissions
91 # Anonymous user should not see issues without permission
91 # Anonymous user should not see issues without permission
92 Role.anonymous.remove_permission!(:view_issues)
92 Role.anonymous.remove_permission!(:view_issues)
93 issues = Issue.visible(User.anonymous).all
93 issues = Issue.visible(User.anonymous).all
94 assert issues.empty?
94 assert issues.empty?
95 assert_visibility_match User.anonymous, issues
95 assert_visibility_match User.anonymous, issues
96 end
96 end
97
97
98 def test_visible_scope_for_non_member
98 def test_visible_scope_for_non_member
99 user = User.find(9)
99 user = User.find(9)
100 assert user.projects.empty?
100 assert user.projects.empty?
101 # Non member user should see issues of public projects only
101 # Non member user should see issues of public projects only
102 issues = Issue.visible(user).all
102 issues = Issue.visible(user).all
103 assert issues.any?
103 assert issues.any?
104 assert_nil issues.detect {|issue| !issue.project.is_public?}
104 assert_nil issues.detect {|issue| !issue.project.is_public?}
105 assert_visibility_match user, issues
105 assert_visibility_match user, issues
106 end
106 end
107
107
108 def test_visible_scope_for_non_member_with_own_issues_visibility
108 def test_visible_scope_for_non_member_with_own_issues_visibility
109 Role.non_member.update_attribute :issues_visibility, 'own'
109 Role.non_member.update_attribute :issues_visibility, 'own'
110 Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 9, :subject => 'Issue by non member')
110 Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 9, :subject => 'Issue by non member')
111 user = User.find(9)
111 user = User.find(9)
112
112
113 issues = Issue.visible(user).all
113 issues = Issue.visible(user).all
114 assert issues.any?
114 assert issues.any?
115 assert_nil issues.detect {|issue| issue.author != user}
115 assert_nil issues.detect {|issue| issue.author != user}
116 assert_visibility_match user, issues
116 assert_visibility_match user, issues
117 end
117 end
118
118
119 def test_visible_scope_for_non_member_without_view_issues_permissions
119 def test_visible_scope_for_non_member_without_view_issues_permissions
120 # Non member user should not see issues without permission
120 # Non member user should not see issues without permission
121 Role.non_member.remove_permission!(:view_issues)
121 Role.non_member.remove_permission!(:view_issues)
122 user = User.find(9)
122 user = User.find(9)
123 assert user.projects.empty?
123 assert user.projects.empty?
124 issues = Issue.visible(user).all
124 issues = Issue.visible(user).all
125 assert issues.empty?
125 assert issues.empty?
126 assert_visibility_match user, issues
126 assert_visibility_match user, issues
127 end
127 end
128
128
129 def test_visible_scope_for_member
129 def test_visible_scope_for_member
130 user = User.find(9)
130 user = User.find(9)
131 # User should see issues of projects for which he has view_issues permissions only
131 # User should see issues of projects for which he has view_issues permissions only
132 Role.non_member.remove_permission!(:view_issues)
132 Role.non_member.remove_permission!(:view_issues)
133 Member.create!(:principal => user, :project_id => 2, :role_ids => [1])
133 Member.create!(:principal => user, :project_id => 2, :role_ids => [1])
134 issues = Issue.visible(user).all
134 issues = Issue.visible(user).all
135 assert issues.any?
135 assert issues.any?
136 assert_nil issues.detect {|issue| issue.project_id != 2}
136 assert_nil issues.detect {|issue| issue.project_id != 2}
137 assert_visibility_match user, issues
137 assert_visibility_match user, issues
138 end
138 end
139
139
140 def test_visible_scope_for_admin
140 def test_visible_scope_for_admin
141 user = User.find(1)
141 user = User.find(1)
142 user.members.each(&:destroy)
142 user.members.each(&:destroy)
143 assert user.projects.empty?
143 assert user.projects.empty?
144 issues = Issue.visible(user).all
144 issues = Issue.visible(user).all
145 assert issues.any?
145 assert issues.any?
146 # Admin should see issues on private projects that he does not belong to
146 # Admin should see issues on private projects that he does not belong to
147 assert issues.detect {|issue| !issue.project.is_public?}
147 assert issues.detect {|issue| !issue.project.is_public?}
148 assert_visibility_match user, issues
148 assert_visibility_match user, issues
149 end
149 end
150
150
151 def test_visible_scope_with_project
151 def test_visible_scope_with_project
152 project = Project.find(1)
152 project = Project.find(1)
153 issues = Issue.visible(User.find(2), :project => project).all
153 issues = Issue.visible(User.find(2), :project => project).all
154 projects = issues.collect(&:project).uniq
154 projects = issues.collect(&:project).uniq
155 assert_equal 1, projects.size
155 assert_equal 1, projects.size
156 assert_equal project, projects.first
156 assert_equal project, projects.first
157 end
157 end
158
158
159 def test_visible_scope_with_project_and_subprojects
159 def test_visible_scope_with_project_and_subprojects
160 project = Project.find(1)
160 project = Project.find(1)
161 issues = Issue.visible(User.find(2), :project => project, :with_subprojects => true).all
161 issues = Issue.visible(User.find(2), :project => project, :with_subprojects => true).all
162 projects = issues.collect(&:project).uniq
162 projects = issues.collect(&:project).uniq
163 assert projects.size > 1
163 assert projects.size > 1
164 assert_equal [], projects.select {|p| !p.is_or_is_descendant_of?(project)}
164 assert_equal [], projects.select {|p| !p.is_or_is_descendant_of?(project)}
165 end
165 end
166
166
167 def test_visible_and_nested_set_scopes
168 assert_equal 0, Issue.find(1).descendants.visible.all.size
169 end
170
167 def test_errors_full_messages_should_include_custom_fields_errors
171 def test_errors_full_messages_should_include_custom_fields_errors
168 field = IssueCustomField.find_by_name('Database')
172 field = IssueCustomField.find_by_name('Database')
169
173
170 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => 'test_create', :description => 'IssueTest#test_create_with_required_custom_field')
174 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => 'test_create', :description => 'IssueTest#test_create_with_required_custom_field')
171 assert issue.available_custom_fields.include?(field)
175 assert issue.available_custom_fields.include?(field)
172 # Invalid value
176 # Invalid value
173 issue.custom_field_values = { field.id => 'SQLServer' }
177 issue.custom_field_values = { field.id => 'SQLServer' }
174
178
175 assert !issue.valid?
179 assert !issue.valid?
176 assert_equal 1, issue.errors.full_messages.size
180 assert_equal 1, issue.errors.full_messages.size
177 assert_equal "Database #{I18n.translate('activerecord.errors.messages.inclusion')}", issue.errors.full_messages.first
181 assert_equal "Database #{I18n.translate('activerecord.errors.messages.inclusion')}", issue.errors.full_messages.first
178 end
182 end
179
183
180 def test_update_issue_with_required_custom_field
184 def test_update_issue_with_required_custom_field
181 field = IssueCustomField.find_by_name('Database')
185 field = IssueCustomField.find_by_name('Database')
182 field.update_attribute(:is_required, true)
186 field.update_attribute(:is_required, true)
183
187
184 issue = Issue.find(1)
188 issue = Issue.find(1)
185 assert_nil issue.custom_value_for(field)
189 assert_nil issue.custom_value_for(field)
186 assert issue.available_custom_fields.include?(field)
190 assert issue.available_custom_fields.include?(field)
187 # No change to custom values, issue can be saved
191 # No change to custom values, issue can be saved
188 assert issue.save
192 assert issue.save
189 # Blank value
193 # Blank value
190 issue.custom_field_values = { field.id => '' }
194 issue.custom_field_values = { field.id => '' }
191 assert !issue.save
195 assert !issue.save
192 # Valid value
196 # Valid value
193 issue.custom_field_values = { field.id => 'PostgreSQL' }
197 issue.custom_field_values = { field.id => 'PostgreSQL' }
194 assert issue.save
198 assert issue.save
195 issue.reload
199 issue.reload
196 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
200 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
197 end
201 end
198
202
199 def test_should_not_update_attributes_if_custom_fields_validation_fails
203 def test_should_not_update_attributes_if_custom_fields_validation_fails
200 issue = Issue.find(1)
204 issue = Issue.find(1)
201 field = IssueCustomField.find_by_name('Database')
205 field = IssueCustomField.find_by_name('Database')
202 assert issue.available_custom_fields.include?(field)
206 assert issue.available_custom_fields.include?(field)
203
207
204 issue.custom_field_values = { field.id => 'Invalid' }
208 issue.custom_field_values = { field.id => 'Invalid' }
205 issue.subject = 'Should be not be saved'
209 issue.subject = 'Should be not be saved'
206 assert !issue.save
210 assert !issue.save
207
211
208 issue.reload
212 issue.reload
209 assert_equal "Can't print recipes", issue.subject
213 assert_equal "Can't print recipes", issue.subject
210 end
214 end
211
215
212 def test_should_not_recreate_custom_values_objects_on_update
216 def test_should_not_recreate_custom_values_objects_on_update
213 field = IssueCustomField.find_by_name('Database')
217 field = IssueCustomField.find_by_name('Database')
214
218
215 issue = Issue.find(1)
219 issue = Issue.find(1)
216 issue.custom_field_values = { field.id => 'PostgreSQL' }
220 issue.custom_field_values = { field.id => 'PostgreSQL' }
217 assert issue.save
221 assert issue.save
218 custom_value = issue.custom_value_for(field)
222 custom_value = issue.custom_value_for(field)
219 issue.reload
223 issue.reload
220 issue.custom_field_values = { field.id => 'MySQL' }
224 issue.custom_field_values = { field.id => 'MySQL' }
221 assert issue.save
225 assert issue.save
222 issue.reload
226 issue.reload
223 assert_equal custom_value.id, issue.custom_value_for(field).id
227 assert_equal custom_value.id, issue.custom_value_for(field).id
224 end
228 end
225
229
226 def test_assigning_tracker_id_should_reload_custom_fields_values
230 def test_assigning_tracker_id_should_reload_custom_fields_values
227 issue = Issue.new(:project => Project.find(1))
231 issue = Issue.new(:project => Project.find(1))
228 assert issue.custom_field_values.empty?
232 assert issue.custom_field_values.empty?
229 issue.tracker_id = 1
233 issue.tracker_id = 1
230 assert issue.custom_field_values.any?
234 assert issue.custom_field_values.any?
231 end
235 end
232
236
233 def test_assigning_attributes_should_assign_tracker_id_first
237 def test_assigning_attributes_should_assign_tracker_id_first
234 attributes = ActiveSupport::OrderedHash.new
238 attributes = ActiveSupport::OrderedHash.new
235 attributes['custom_field_values'] = { '1' => 'MySQL' }
239 attributes['custom_field_values'] = { '1' => 'MySQL' }
236 attributes['tracker_id'] = '1'
240 attributes['tracker_id'] = '1'
237 issue = Issue.new(:project => Project.find(1))
241 issue = Issue.new(:project => Project.find(1))
238 issue.attributes = attributes
242 issue.attributes = attributes
239 assert_not_nil issue.custom_value_for(1)
243 assert_not_nil issue.custom_value_for(1)
240 assert_equal 'MySQL', issue.custom_value_for(1).value
244 assert_equal 'MySQL', issue.custom_value_for(1).value
241 end
245 end
242
246
243 def test_should_update_issue_with_disabled_tracker
247 def test_should_update_issue_with_disabled_tracker
244 p = Project.find(1)
248 p = Project.find(1)
245 issue = Issue.find(1)
249 issue = Issue.find(1)
246
250
247 p.trackers.delete(issue.tracker)
251 p.trackers.delete(issue.tracker)
248 assert !p.trackers.include?(issue.tracker)
252 assert !p.trackers.include?(issue.tracker)
249
253
250 issue.reload
254 issue.reload
251 issue.subject = 'New subject'
255 issue.subject = 'New subject'
252 assert issue.save
256 assert issue.save
253 end
257 end
254
258
255 def test_should_not_set_a_disabled_tracker
259 def test_should_not_set_a_disabled_tracker
256 p = Project.find(1)
260 p = Project.find(1)
257 p.trackers.delete(Tracker.find(2))
261 p.trackers.delete(Tracker.find(2))
258
262
259 issue = Issue.find(1)
263 issue = Issue.find(1)
260 issue.tracker_id = 2
264 issue.tracker_id = 2
261 issue.subject = 'New subject'
265 issue.subject = 'New subject'
262 assert !issue.save
266 assert !issue.save
263 assert_not_nil issue.errors.on(:tracker_id)
267 assert_not_nil issue.errors.on(:tracker_id)
264 end
268 end
265
269
266 def test_category_based_assignment
270 def test_category_based_assignment
267 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'Assignment test', :description => 'Assignment test', :category_id => 1)
271 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'Assignment test', :description => 'Assignment test', :category_id => 1)
268 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
272 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
269 end
273 end
270
274
271
275
272
276
273 def test_new_statuses_allowed_to
277 def test_new_statuses_allowed_to
274 Workflow.delete_all
278 Workflow.delete_all
275
279
276 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 2, :author => false, :assignee => false)
280 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 2, :author => false, :assignee => false)
277 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 3, :author => true, :assignee => false)
281 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 3, :author => true, :assignee => false)
278 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 4, :author => false, :assignee => true)
282 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 4, :author => false, :assignee => true)
279 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 5, :author => true, :assignee => true)
283 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 5, :author => true, :assignee => true)
280 status = IssueStatus.find(1)
284 status = IssueStatus.find(1)
281 role = Role.find(1)
285 role = Role.find(1)
282 tracker = Tracker.find(1)
286 tracker = Tracker.find(1)
283 user = User.find(2)
287 user = User.find(2)
284
288
285 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1)
289 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1)
286 assert_equal [1, 2], issue.new_statuses_allowed_to(user).map(&:id)
290 assert_equal [1, 2], issue.new_statuses_allowed_to(user).map(&:id)
287
291
288 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1, :author => user)
292 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1, :author => user)
289 assert_equal [1, 2, 3], issue.new_statuses_allowed_to(user).map(&:id)
293 assert_equal [1, 2, 3], issue.new_statuses_allowed_to(user).map(&:id)
290
294
291 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1, :assigned_to => user)
295 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1, :assigned_to => user)
292 assert_equal [1, 2, 4], issue.new_statuses_allowed_to(user).map(&:id)
296 assert_equal [1, 2, 4], issue.new_statuses_allowed_to(user).map(&:id)
293
297
294 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1, :author => user, :assigned_to => user)
298 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1, :author => user, :assigned_to => user)
295 assert_equal [1, 2, 3, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
299 assert_equal [1, 2, 3, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
296 end
300 end
297
301
298 def test_copy
302 def test_copy
299 issue = Issue.new.copy_from(1)
303 issue = Issue.new.copy_from(1)
300 assert issue.save
304 assert issue.save
301 issue.reload
305 issue.reload
302 orig = Issue.find(1)
306 orig = Issue.find(1)
303 assert_equal orig.subject, issue.subject
307 assert_equal orig.subject, issue.subject
304 assert_equal orig.tracker, issue.tracker
308 assert_equal orig.tracker, issue.tracker
305 assert_equal "125", issue.custom_value_for(2).value
309 assert_equal "125", issue.custom_value_for(2).value
306 end
310 end
307
311
308 def test_copy_should_copy_status
312 def test_copy_should_copy_status
309 orig = Issue.find(8)
313 orig = Issue.find(8)
310 assert orig.status != IssueStatus.default
314 assert orig.status != IssueStatus.default
311
315
312 issue = Issue.new.copy_from(orig)
316 issue = Issue.new.copy_from(orig)
313 assert issue.save
317 assert issue.save
314 issue.reload
318 issue.reload
315 assert_equal orig.status, issue.status
319 assert_equal orig.status, issue.status
316 end
320 end
317
321
318 def test_should_close_duplicates
322 def test_should_close_duplicates
319 # Create 3 issues
323 # Create 3 issues
320 issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'Duplicates test', :description => 'Duplicates test')
324 issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'Duplicates test', :description => 'Duplicates test')
321 assert issue1.save
325 assert issue1.save
322 issue2 = issue1.clone
326 issue2 = issue1.clone
323 assert issue2.save
327 assert issue2.save
324 issue3 = issue1.clone
328 issue3 = issue1.clone
325 assert issue3.save
329 assert issue3.save
326
330
327 # 2 is a dupe of 1
331 # 2 is a dupe of 1
328 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
332 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
329 # And 3 is a dupe of 2
333 # And 3 is a dupe of 2
330 IssueRelation.create(:issue_from => issue3, :issue_to => issue2, :relation_type => IssueRelation::TYPE_DUPLICATES)
334 IssueRelation.create(:issue_from => issue3, :issue_to => issue2, :relation_type => IssueRelation::TYPE_DUPLICATES)
331 # And 3 is a dupe of 1 (circular duplicates)
335 # And 3 is a dupe of 1 (circular duplicates)
332 IssueRelation.create(:issue_from => issue3, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
336 IssueRelation.create(:issue_from => issue3, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
333
337
334 assert issue1.reload.duplicates.include?(issue2)
338 assert issue1.reload.duplicates.include?(issue2)
335
339
336 # Closing issue 1
340 # Closing issue 1
337 issue1.init_journal(User.find(:first), "Closing issue1")
341 issue1.init_journal(User.find(:first), "Closing issue1")
338 issue1.status = IssueStatus.find :first, :conditions => {:is_closed => true}
342 issue1.status = IssueStatus.find :first, :conditions => {:is_closed => true}
339 assert issue1.save
343 assert issue1.save
340 # 2 and 3 should be also closed
344 # 2 and 3 should be also closed
341 assert issue2.reload.closed?
345 assert issue2.reload.closed?
342 assert issue3.reload.closed?
346 assert issue3.reload.closed?
343 end
347 end
344
348
345 def test_should_not_close_duplicated_issue
349 def test_should_not_close_duplicated_issue
346 # Create 3 issues
350 # Create 3 issues
347 issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'Duplicates test', :description => 'Duplicates test')
351 issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'Duplicates test', :description => 'Duplicates test')
348 assert issue1.save
352 assert issue1.save
349 issue2 = issue1.clone
353 issue2 = issue1.clone
350 assert issue2.save
354 assert issue2.save
351
355
352 # 2 is a dupe of 1
356 # 2 is a dupe of 1
353 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
357 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
354 # 2 is a dup of 1 but 1 is not a duplicate of 2
358 # 2 is a dup of 1 but 1 is not a duplicate of 2
355 assert !issue2.reload.duplicates.include?(issue1)
359 assert !issue2.reload.duplicates.include?(issue1)
356
360
357 # Closing issue 2
361 # Closing issue 2
358 issue2.init_journal(User.find(:first), "Closing issue2")
362 issue2.init_journal(User.find(:first), "Closing issue2")
359 issue2.status = IssueStatus.find :first, :conditions => {:is_closed => true}
363 issue2.status = IssueStatus.find :first, :conditions => {:is_closed => true}
360 assert issue2.save
364 assert issue2.save
361 # 1 should not be also closed
365 # 1 should not be also closed
362 assert !issue1.reload.closed?
366 assert !issue1.reload.closed?
363 end
367 end
364
368
365 def test_assignable_versions
369 def test_assignable_versions
366 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
370 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
367 assert_equal ['open'], issue.assignable_versions.collect(&:status).uniq
371 assert_equal ['open'], issue.assignable_versions.collect(&:status).uniq
368 end
372 end
369
373
370 def test_should_not_be_able_to_assign_a_new_issue_to_a_closed_version
374 def test_should_not_be_able_to_assign_a_new_issue_to_a_closed_version
371 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
375 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
372 assert !issue.save
376 assert !issue.save
373 assert_not_nil issue.errors.on(:fixed_version_id)
377 assert_not_nil issue.errors.on(:fixed_version_id)
374 end
378 end
375
379
376 def test_should_not_be_able_to_assign_a_new_issue_to_a_locked_version
380 def test_should_not_be_able_to_assign_a_new_issue_to_a_locked_version
377 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 2, :subject => 'New issue')
381 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 2, :subject => 'New issue')
378 assert !issue.save
382 assert !issue.save
379 assert_not_nil issue.errors.on(:fixed_version_id)
383 assert_not_nil issue.errors.on(:fixed_version_id)
380 end
384 end
381
385
382 def test_should_be_able_to_assign_a_new_issue_to_an_open_version
386 def test_should_be_able_to_assign_a_new_issue_to_an_open_version
383 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 3, :subject => 'New issue')
387 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 3, :subject => 'New issue')
384 assert issue.save
388 assert issue.save
385 end
389 end
386
390
387 def test_should_be_able_to_update_an_issue_assigned_to_a_closed_version
391 def test_should_be_able_to_update_an_issue_assigned_to_a_closed_version
388 issue = Issue.find(11)
392 issue = Issue.find(11)
389 assert_equal 'closed', issue.fixed_version.status
393 assert_equal 'closed', issue.fixed_version.status
390 issue.subject = 'Subject changed'
394 issue.subject = 'Subject changed'
391 assert issue.save
395 assert issue.save
392 end
396 end
393
397
394 def test_should_not_be_able_to_reopen_an_issue_assigned_to_a_closed_version
398 def test_should_not_be_able_to_reopen_an_issue_assigned_to_a_closed_version
395 issue = Issue.find(11)
399 issue = Issue.find(11)
396 issue.status_id = 1
400 issue.status_id = 1
397 assert !issue.save
401 assert !issue.save
398 assert_not_nil issue.errors.on_base
402 assert_not_nil issue.errors.on_base
399 end
403 end
400
404
401 def test_should_be_able_to_reopen_and_reassign_an_issue_assigned_to_a_closed_version
405 def test_should_be_able_to_reopen_and_reassign_an_issue_assigned_to_a_closed_version
402 issue = Issue.find(11)
406 issue = Issue.find(11)
403 issue.status_id = 1
407 issue.status_id = 1
404 issue.fixed_version_id = 3
408 issue.fixed_version_id = 3
405 assert issue.save
409 assert issue.save
406 end
410 end
407
411
408 def test_should_be_able_to_reopen_an_issue_assigned_to_a_locked_version
412 def test_should_be_able_to_reopen_an_issue_assigned_to_a_locked_version
409 issue = Issue.find(12)
413 issue = Issue.find(12)
410 assert_equal 'locked', issue.fixed_version.status
414 assert_equal 'locked', issue.fixed_version.status
411 issue.status_id = 1
415 issue.status_id = 1
412 assert issue.save
416 assert issue.save
413 end
417 end
414
418
415 def test_move_to_another_project_with_same_category
419 def test_move_to_another_project_with_same_category
416 issue = Issue.find(1)
420 issue = Issue.find(1)
417 assert issue.move_to_project(Project.find(2))
421 assert issue.move_to_project(Project.find(2))
418 issue.reload
422 issue.reload
419 assert_equal 2, issue.project_id
423 assert_equal 2, issue.project_id
420 # Category changes
424 # Category changes
421 assert_equal 4, issue.category_id
425 assert_equal 4, issue.category_id
422 # Make sure time entries were move to the target project
426 # Make sure time entries were move to the target project
423 assert_equal 2, issue.time_entries.first.project_id
427 assert_equal 2, issue.time_entries.first.project_id
424 end
428 end
425
429
426 def test_move_to_another_project_without_same_category
430 def test_move_to_another_project_without_same_category
427 issue = Issue.find(2)
431 issue = Issue.find(2)
428 assert issue.move_to_project(Project.find(2))
432 assert issue.move_to_project(Project.find(2))
429 issue.reload
433 issue.reload
430 assert_equal 2, issue.project_id
434 assert_equal 2, issue.project_id
431 # Category cleared
435 # Category cleared
432 assert_nil issue.category_id
436 assert_nil issue.category_id
433 end
437 end
434
438
435 def test_move_to_another_project_should_clear_fixed_version_when_not_shared
439 def test_move_to_another_project_should_clear_fixed_version_when_not_shared
436 issue = Issue.find(1)
440 issue = Issue.find(1)
437 issue.update_attribute(:fixed_version_id, 1)
441 issue.update_attribute(:fixed_version_id, 1)
438 assert issue.move_to_project(Project.find(2))
442 assert issue.move_to_project(Project.find(2))
439 issue.reload
443 issue.reload
440 assert_equal 2, issue.project_id
444 assert_equal 2, issue.project_id
441 # Cleared fixed_version
445 # Cleared fixed_version
442 assert_equal nil, issue.fixed_version
446 assert_equal nil, issue.fixed_version
443 end
447 end
444
448
445 def test_move_to_another_project_should_keep_fixed_version_when_shared_with_the_target_project
449 def test_move_to_another_project_should_keep_fixed_version_when_shared_with_the_target_project
446 issue = Issue.find(1)
450 issue = Issue.find(1)
447 issue.update_attribute(:fixed_version_id, 4)
451 issue.update_attribute(:fixed_version_id, 4)
448 assert issue.move_to_project(Project.find(5))
452 assert issue.move_to_project(Project.find(5))
449 issue.reload
453 issue.reload
450 assert_equal 5, issue.project_id
454 assert_equal 5, issue.project_id
451 # Keep fixed_version
455 # Keep fixed_version
452 assert_equal 4, issue.fixed_version_id
456 assert_equal 4, issue.fixed_version_id
453 end
457 end
454
458
455 def test_move_to_another_project_should_clear_fixed_version_when_not_shared_with_the_target_project
459 def test_move_to_another_project_should_clear_fixed_version_when_not_shared_with_the_target_project
456 issue = Issue.find(1)
460 issue = Issue.find(1)
457 issue.update_attribute(:fixed_version_id, 1)
461 issue.update_attribute(:fixed_version_id, 1)
458 assert issue.move_to_project(Project.find(5))
462 assert issue.move_to_project(Project.find(5))
459 issue.reload
463 issue.reload
460 assert_equal 5, issue.project_id
464 assert_equal 5, issue.project_id
461 # Cleared fixed_version
465 # Cleared fixed_version
462 assert_equal nil, issue.fixed_version
466 assert_equal nil, issue.fixed_version
463 end
467 end
464
468
465 def test_move_to_another_project_should_keep_fixed_version_when_shared_systemwide
469 def test_move_to_another_project_should_keep_fixed_version_when_shared_systemwide
466 issue = Issue.find(1)
470 issue = Issue.find(1)
467 issue.update_attribute(:fixed_version_id, 7)
471 issue.update_attribute(:fixed_version_id, 7)
468 assert issue.move_to_project(Project.find(2))
472 assert issue.move_to_project(Project.find(2))
469 issue.reload
473 issue.reload
470 assert_equal 2, issue.project_id
474 assert_equal 2, issue.project_id
471 # Keep fixed_version
475 # Keep fixed_version
472 assert_equal 7, issue.fixed_version_id
476 assert_equal 7, issue.fixed_version_id
473 end
477 end
474
478
475 def test_move_to_another_project_with_disabled_tracker
479 def test_move_to_another_project_with_disabled_tracker
476 issue = Issue.find(1)
480 issue = Issue.find(1)
477 target = Project.find(2)
481 target = Project.find(2)
478 target.tracker_ids = [3]
482 target.tracker_ids = [3]
479 target.save
483 target.save
480 assert_equal false, issue.move_to_project(target)
484 assert_equal false, issue.move_to_project(target)
481 issue.reload
485 issue.reload
482 assert_equal 1, issue.project_id
486 assert_equal 1, issue.project_id
483 end
487 end
484
488
485 def test_copy_to_the_same_project
489 def test_copy_to_the_same_project
486 issue = Issue.find(1)
490 issue = Issue.find(1)
487 copy = nil
491 copy = nil
488 assert_difference 'Issue.count' do
492 assert_difference 'Issue.count' do
489 copy = issue.move_to_project(issue.project, nil, :copy => true)
493 copy = issue.move_to_project(issue.project, nil, :copy => true)
490 end
494 end
491 assert_kind_of Issue, copy
495 assert_kind_of Issue, copy
492 assert_equal issue.project, copy.project
496 assert_equal issue.project, copy.project
493 assert_equal "125", copy.custom_value_for(2).value
497 assert_equal "125", copy.custom_value_for(2).value
494 end
498 end
495
499
496 def test_copy_to_another_project_and_tracker
500 def test_copy_to_another_project_and_tracker
497 issue = Issue.find(1)
501 issue = Issue.find(1)
498 copy = nil
502 copy = nil
499 assert_difference 'Issue.count' do
503 assert_difference 'Issue.count' do
500 copy = issue.move_to_project(Project.find(3), Tracker.find(2), :copy => true)
504 copy = issue.move_to_project(Project.find(3), Tracker.find(2), :copy => true)
501 end
505 end
502 copy.reload
506 copy.reload
503 assert_kind_of Issue, copy
507 assert_kind_of Issue, copy
504 assert_equal Project.find(3), copy.project
508 assert_equal Project.find(3), copy.project
505 assert_equal Tracker.find(2), copy.tracker
509 assert_equal Tracker.find(2), copy.tracker
506 # Custom field #2 is not associated with target tracker
510 # Custom field #2 is not associated with target tracker
507 assert_nil copy.custom_value_for(2)
511 assert_nil copy.custom_value_for(2)
508 end
512 end
509
513
510 context "#move_to_project" do
514 context "#move_to_project" do
511 context "as a copy" do
515 context "as a copy" do
512 setup do
516 setup do
513 @issue = Issue.find(1)
517 @issue = Issue.find(1)
514 @copy = nil
518 @copy = nil
515 end
519 end
516
520
517 should "allow assigned_to changes" do
521 should "allow assigned_to changes" do
518 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:assigned_to_id => 3}})
522 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:assigned_to_id => 3}})
519 assert_equal 3, @copy.assigned_to_id
523 assert_equal 3, @copy.assigned_to_id
520 end
524 end
521
525
522 should "allow status changes" do
526 should "allow status changes" do
523 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:status_id => 2}})
527 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:status_id => 2}})
524 assert_equal 2, @copy.status_id
528 assert_equal 2, @copy.status_id
525 end
529 end
526
530
527 should "allow start date changes" do
531 should "allow start date changes" do
528 date = Date.today
532 date = Date.today
529 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:start_date => date}})
533 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:start_date => date}})
530 assert_equal date, @copy.start_date
534 assert_equal date, @copy.start_date
531 end
535 end
532
536
533 should "allow due date changes" do
537 should "allow due date changes" do
534 date = Date.today
538 date = Date.today
535 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:due_date => date}})
539 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:due_date => date}})
536
540
537 assert_equal date, @copy.due_date
541 assert_equal date, @copy.due_date
538 end
542 end
539 end
543 end
540 end
544 end
541
545
542 def test_recipients_should_not_include_users_that_cannot_view_the_issue
546 def test_recipients_should_not_include_users_that_cannot_view_the_issue
543 issue = Issue.find(12)
547 issue = Issue.find(12)
544 assert issue.recipients.include?(issue.author.mail)
548 assert issue.recipients.include?(issue.author.mail)
545 # move the issue to a private project
549 # move the issue to a private project
546 copy = issue.move_to_project(Project.find(5), Tracker.find(2), :copy => true)
550 copy = issue.move_to_project(Project.find(5), Tracker.find(2), :copy => true)
547 # author is not a member of project anymore
551 # author is not a member of project anymore
548 assert !copy.recipients.include?(copy.author.mail)
552 assert !copy.recipients.include?(copy.author.mail)
549 end
553 end
550
554
551 def test_watcher_recipients_should_not_include_users_that_cannot_view_the_issue
555 def test_watcher_recipients_should_not_include_users_that_cannot_view_the_issue
552 user = User.find(3)
556 user = User.find(3)
553 issue = Issue.find(9)
557 issue = Issue.find(9)
554 Watcher.create!(:user => user, :watchable => issue)
558 Watcher.create!(:user => user, :watchable => issue)
555 assert issue.watched_by?(user)
559 assert issue.watched_by?(user)
556 assert !issue.watcher_recipients.include?(user.mail)
560 assert !issue.watcher_recipients.include?(user.mail)
557 end
561 end
558
562
559 def test_issue_destroy
563 def test_issue_destroy
560 Issue.find(1).destroy
564 Issue.find(1).destroy
561 assert_nil Issue.find_by_id(1)
565 assert_nil Issue.find_by_id(1)
562 assert_nil TimeEntry.find_by_issue_id(1)
566 assert_nil TimeEntry.find_by_issue_id(1)
563 end
567 end
564
568
565 def test_blocked
569 def test_blocked
566 blocked_issue = Issue.find(9)
570 blocked_issue = Issue.find(9)
567 blocking_issue = Issue.find(10)
571 blocking_issue = Issue.find(10)
568
572
569 assert blocked_issue.blocked?
573 assert blocked_issue.blocked?
570 assert !blocking_issue.blocked?
574 assert !blocking_issue.blocked?
571 end
575 end
572
576
573 def test_blocked_issues_dont_allow_closed_statuses
577 def test_blocked_issues_dont_allow_closed_statuses
574 blocked_issue = Issue.find(9)
578 blocked_issue = Issue.find(9)
575
579
576 allowed_statuses = blocked_issue.new_statuses_allowed_to(users(:users_002))
580 allowed_statuses = blocked_issue.new_statuses_allowed_to(users(:users_002))
577 assert !allowed_statuses.empty?
581 assert !allowed_statuses.empty?
578 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
582 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
579 assert closed_statuses.empty?
583 assert closed_statuses.empty?
580 end
584 end
581
585
582 def test_unblocked_issues_allow_closed_statuses
586 def test_unblocked_issues_allow_closed_statuses
583 blocking_issue = Issue.find(10)
587 blocking_issue = Issue.find(10)
584
588
585 allowed_statuses = blocking_issue.new_statuses_allowed_to(users(:users_002))
589 allowed_statuses = blocking_issue.new_statuses_allowed_to(users(:users_002))
586 assert !allowed_statuses.empty?
590 assert !allowed_statuses.empty?
587 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
591 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
588 assert !closed_statuses.empty?
592 assert !closed_statuses.empty?
589 end
593 end
590
594
591 def test_rescheduling_an_issue_should_reschedule_following_issue
595 def test_rescheduling_an_issue_should_reschedule_following_issue
592 issue1 = Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => '-', :start_date => Date.today, :due_date => Date.today + 2)
596 issue1 = Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => '-', :start_date => Date.today, :due_date => Date.today + 2)
593 issue2 = Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => '-', :start_date => Date.today, :due_date => Date.today + 2)
597 issue2 = Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => '-', :start_date => Date.today, :due_date => Date.today + 2)
594 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_PRECEDES)
598 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_PRECEDES)
595 assert_equal issue1.due_date + 1, issue2.reload.start_date
599 assert_equal issue1.due_date + 1, issue2.reload.start_date
596
600
597 issue1.due_date = Date.today + 5
601 issue1.due_date = Date.today + 5
598 issue1.save!
602 issue1.save!
599 assert_equal issue1.due_date + 1, issue2.reload.start_date
603 assert_equal issue1.due_date + 1, issue2.reload.start_date
600 end
604 end
601
605
602 def test_overdue
606 def test_overdue
603 assert Issue.new(:due_date => 1.day.ago.to_date).overdue?
607 assert Issue.new(:due_date => 1.day.ago.to_date).overdue?
604 assert !Issue.new(:due_date => Date.today).overdue?
608 assert !Issue.new(:due_date => Date.today).overdue?
605 assert !Issue.new(:due_date => 1.day.from_now.to_date).overdue?
609 assert !Issue.new(:due_date => 1.day.from_now.to_date).overdue?
606 assert !Issue.new(:due_date => nil).overdue?
610 assert !Issue.new(:due_date => nil).overdue?
607 assert !Issue.new(:due_date => 1.day.ago.to_date, :status => IssueStatus.find(:first, :conditions => {:is_closed => true})).overdue?
611 assert !Issue.new(:due_date => 1.day.ago.to_date, :status => IssueStatus.find(:first, :conditions => {:is_closed => true})).overdue?
608 end
612 end
609
613
610 context "#behind_schedule?" do
614 context "#behind_schedule?" do
611 should "be false if the issue has no start_date" do
615 should "be false if the issue has no start_date" do
612 assert !Issue.new(:start_date => nil, :due_date => 1.day.from_now.to_date, :done_ratio => 0).behind_schedule?
616 assert !Issue.new(:start_date => nil, :due_date => 1.day.from_now.to_date, :done_ratio => 0).behind_schedule?
613 end
617 end
614
618
615 should "be false if the issue has no end_date" do
619 should "be false if the issue has no end_date" do
616 assert !Issue.new(:start_date => 1.day.from_now.to_date, :due_date => nil, :done_ratio => 0).behind_schedule?
620 assert !Issue.new(:start_date => 1.day.from_now.to_date, :due_date => nil, :done_ratio => 0).behind_schedule?
617 end
621 end
618
622
619 should "be false if the issue has more done than it's calendar time" do
623 should "be false if the issue has more done than it's calendar time" do
620 assert !Issue.new(:start_date => 50.days.ago.to_date, :due_date => 50.days.from_now.to_date, :done_ratio => 90).behind_schedule?
624 assert !Issue.new(:start_date => 50.days.ago.to_date, :due_date => 50.days.from_now.to_date, :done_ratio => 90).behind_schedule?
621 end
625 end
622
626
623 should "be true if the issue hasn't been started at all" do
627 should "be true if the issue hasn't been started at all" do
624 assert Issue.new(:start_date => 1.day.ago.to_date, :due_date => 1.day.from_now.to_date, :done_ratio => 0).behind_schedule?
628 assert Issue.new(:start_date => 1.day.ago.to_date, :due_date => 1.day.from_now.to_date, :done_ratio => 0).behind_schedule?
625 end
629 end
626
630
627 should "be true if the issue has used more calendar time than it's done ratio" do
631 should "be true if the issue has used more calendar time than it's done ratio" do
628 assert Issue.new(:start_date => 100.days.ago.to_date, :due_date => Date.today, :done_ratio => 90).behind_schedule?
632 assert Issue.new(:start_date => 100.days.ago.to_date, :due_date => Date.today, :done_ratio => 90).behind_schedule?
629 end
633 end
630 end
634 end
631
635
632 context "#assignable_users" do
636 context "#assignable_users" do
633 should "be Users" do
637 should "be Users" do
634 assert_kind_of User, Issue.find(1).assignable_users.first
638 assert_kind_of User, Issue.find(1).assignable_users.first
635 end
639 end
636
640
637 should "include the issue author" do
641 should "include the issue author" do
638 project = Project.find(1)
642 project = Project.find(1)
639 non_project_member = User.generate!
643 non_project_member = User.generate!
640 issue = Issue.generate_for_project!(project, :author => non_project_member)
644 issue = Issue.generate_for_project!(project, :author => non_project_member)
641
645
642 assert issue.assignable_users.include?(non_project_member)
646 assert issue.assignable_users.include?(non_project_member)
643 end
647 end
644
648
645 should "not show the issue author twice" do
649 should "not show the issue author twice" do
646 assignable_user_ids = Issue.find(1).assignable_users.collect(&:id)
650 assignable_user_ids = Issue.find(1).assignable_users.collect(&:id)
647 assert_equal 2, assignable_user_ids.length
651 assert_equal 2, assignable_user_ids.length
648
652
649 assignable_user_ids.each do |user_id|
653 assignable_user_ids.each do |user_id|
650 assert_equal 1, assignable_user_ids.select {|i| i == user_id}.length, "User #{user_id} appears more or less than once"
654 assert_equal 1, assignable_user_ids.select {|i| i == user_id}.length, "User #{user_id} appears more or less than once"
651 end
655 end
652 end
656 end
653 end
657 end
654
658
655 def test_create_should_send_email_notification
659 def test_create_should_send_email_notification
656 ActionMailer::Base.deliveries.clear
660 ActionMailer::Base.deliveries.clear
657 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'test_create', :estimated_hours => '1:30')
661 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'test_create', :estimated_hours => '1:30')
658
662
659 assert issue.save
663 assert issue.save
660 assert_equal 1, ActionMailer::Base.deliveries.size
664 assert_equal 1, ActionMailer::Base.deliveries.size
661 end
665 end
662
666
663 def test_stale_issue_should_not_send_email_notification
667 def test_stale_issue_should_not_send_email_notification
664 ActionMailer::Base.deliveries.clear
668 ActionMailer::Base.deliveries.clear
665 issue = Issue.find(1)
669 issue = Issue.find(1)
666 stale = Issue.find(1)
670 stale = Issue.find(1)
667
671
668 issue.init_journal(User.find(1))
672 issue.init_journal(User.find(1))
669 issue.subject = 'Subjet update'
673 issue.subject = 'Subjet update'
670 assert issue.save
674 assert issue.save
671 assert_equal 1, ActionMailer::Base.deliveries.size
675 assert_equal 1, ActionMailer::Base.deliveries.size
672 ActionMailer::Base.deliveries.clear
676 ActionMailer::Base.deliveries.clear
673
677
674 stale.init_journal(User.find(1))
678 stale.init_journal(User.find(1))
675 stale.subject = 'Another subjet update'
679 stale.subject = 'Another subjet update'
676 assert_raise ActiveRecord::StaleObjectError do
680 assert_raise ActiveRecord::StaleObjectError do
677 stale.save
681 stale.save
678 end
682 end
679 assert ActionMailer::Base.deliveries.empty?
683 assert ActionMailer::Base.deliveries.empty?
680 end
684 end
681
685
682 def test_journalized_description
686 def test_journalized_description
683 IssueCustomField.delete_all
687 IssueCustomField.delete_all
684
688
685 i = Issue.first
689 i = Issue.first
686 old_description = i.description
690 old_description = i.description
687 new_description = "This is the new description"
691 new_description = "This is the new description"
688
692
689 i.init_journal(User.find(2))
693 i.init_journal(User.find(2))
690 i.description = new_description
694 i.description = new_description
691 assert_difference 'Journal.count', 1 do
695 assert_difference 'Journal.count', 1 do
692 assert_difference 'JournalDetail.count', 1 do
696 assert_difference 'JournalDetail.count', 1 do
693 i.save!
697 i.save!
694 end
698 end
695 end
699 end
696
700
697 detail = JournalDetail.first(:order => 'id DESC')
701 detail = JournalDetail.first(:order => 'id DESC')
698 assert_equal i, detail.journal.journalized
702 assert_equal i, detail.journal.journalized
699 assert_equal 'attr', detail.property
703 assert_equal 'attr', detail.property
700 assert_equal 'description', detail.prop_key
704 assert_equal 'description', detail.prop_key
701 assert_equal old_description, detail.old_value
705 assert_equal old_description, detail.old_value
702 assert_equal new_description, detail.value
706 assert_equal new_description, detail.value
703 end
707 end
704
708
705 def test_saving_twice_should_not_duplicate_journal_details
709 def test_saving_twice_should_not_duplicate_journal_details
706 i = Issue.find(:first)
710 i = Issue.find(:first)
707 i.init_journal(User.find(2), 'Some notes')
711 i.init_journal(User.find(2), 'Some notes')
708 # initial changes
712 # initial changes
709 i.subject = 'New subject'
713 i.subject = 'New subject'
710 i.done_ratio = i.done_ratio + 10
714 i.done_ratio = i.done_ratio + 10
711 assert_difference 'Journal.count' do
715 assert_difference 'Journal.count' do
712 assert i.save
716 assert i.save
713 end
717 end
714 # 1 more change
718 # 1 more change
715 i.priority = IssuePriority.find(:first, :conditions => ["id <> ?", i.priority_id])
719 i.priority = IssuePriority.find(:first, :conditions => ["id <> ?", i.priority_id])
716 assert_no_difference 'Journal.count' do
720 assert_no_difference 'Journal.count' do
717 assert_difference 'JournalDetail.count', 1 do
721 assert_difference 'JournalDetail.count', 1 do
718 i.save
722 i.save
719 end
723 end
720 end
724 end
721 # no more change
725 # no more change
722 assert_no_difference 'Journal.count' do
726 assert_no_difference 'Journal.count' do
723 assert_no_difference 'JournalDetail.count' do
727 assert_no_difference 'JournalDetail.count' do
724 i.save
728 i.save
725 end
729 end
726 end
730 end
727 end
731 end
728
732
729 def test_all_dependent_issues
733 def test_all_dependent_issues
730 IssueRelation.delete_all
734 IssueRelation.delete_all
731 assert IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(2), :relation_type => IssueRelation::TYPE_PRECEDES)
735 assert IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(2), :relation_type => IssueRelation::TYPE_PRECEDES)
732 assert IssueRelation.create!(:issue_from => Issue.find(2), :issue_to => Issue.find(3), :relation_type => IssueRelation::TYPE_PRECEDES)
736 assert IssueRelation.create!(:issue_from => Issue.find(2), :issue_to => Issue.find(3), :relation_type => IssueRelation::TYPE_PRECEDES)
733 assert IssueRelation.create!(:issue_from => Issue.find(3), :issue_to => Issue.find(8), :relation_type => IssueRelation::TYPE_PRECEDES)
737 assert IssueRelation.create!(:issue_from => Issue.find(3), :issue_to => Issue.find(8), :relation_type => IssueRelation::TYPE_PRECEDES)
734
738
735 assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort
739 assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort
736 end
740 end
737
741
738 def test_all_dependent_issues_with_persistent_circular_dependency
742 def test_all_dependent_issues_with_persistent_circular_dependency
739 IssueRelation.delete_all
743 IssueRelation.delete_all
740 assert IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(2), :relation_type => IssueRelation::TYPE_PRECEDES)
744 assert IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(2), :relation_type => IssueRelation::TYPE_PRECEDES)
741 assert IssueRelation.create!(:issue_from => Issue.find(2), :issue_to => Issue.find(3), :relation_type => IssueRelation::TYPE_PRECEDES)
745 assert IssueRelation.create!(:issue_from => Issue.find(2), :issue_to => Issue.find(3), :relation_type => IssueRelation::TYPE_PRECEDES)
742 # Validation skipping
746 # Validation skipping
743 assert IssueRelation.new(:issue_from => Issue.find(3), :issue_to => Issue.find(1), :relation_type => IssueRelation::TYPE_PRECEDES).save(false)
747 assert IssueRelation.new(:issue_from => Issue.find(3), :issue_to => Issue.find(1), :relation_type => IssueRelation::TYPE_PRECEDES).save(false)
744
748
745 assert_equal [2, 3], Issue.find(1).all_dependent_issues.collect(&:id).sort
749 assert_equal [2, 3], Issue.find(1).all_dependent_issues.collect(&:id).sort
746 end
750 end
747
751
748 def test_all_dependent_issues_with_persistent_multiple_circular_dependencies
752 def test_all_dependent_issues_with_persistent_multiple_circular_dependencies
749 IssueRelation.delete_all
753 IssueRelation.delete_all
750 assert IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(2), :relation_type => IssueRelation::TYPE_RELATES)
754 assert IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(2), :relation_type => IssueRelation::TYPE_RELATES)
751 assert IssueRelation.create!(:issue_from => Issue.find(2), :issue_to => Issue.find(3), :relation_type => IssueRelation::TYPE_RELATES)
755 assert IssueRelation.create!(:issue_from => Issue.find(2), :issue_to => Issue.find(3), :relation_type => IssueRelation::TYPE_RELATES)
752 assert IssueRelation.create!(:issue_from => Issue.find(3), :issue_to => Issue.find(8), :relation_type => IssueRelation::TYPE_RELATES)
756 assert IssueRelation.create!(:issue_from => Issue.find(3), :issue_to => Issue.find(8), :relation_type => IssueRelation::TYPE_RELATES)
753 # Validation skipping
757 # Validation skipping
754 assert IssueRelation.new(:issue_from => Issue.find(8), :issue_to => Issue.find(2), :relation_type => IssueRelation::TYPE_RELATES).save(false)
758 assert IssueRelation.new(:issue_from => Issue.find(8), :issue_to => Issue.find(2), :relation_type => IssueRelation::TYPE_RELATES).save(false)
755 assert IssueRelation.new(:issue_from => Issue.find(3), :issue_to => Issue.find(1), :relation_type => IssueRelation::TYPE_RELATES).save(false)
759 assert IssueRelation.new(:issue_from => Issue.find(3), :issue_to => Issue.find(1), :relation_type => IssueRelation::TYPE_RELATES).save(false)
756
760
757 assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort
761 assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort
758 end
762 end
759
763
760 context "#done_ratio" do
764 context "#done_ratio" do
761 setup do
765 setup do
762 @issue = Issue.find(1)
766 @issue = Issue.find(1)
763 @issue_status = IssueStatus.find(1)
767 @issue_status = IssueStatus.find(1)
764 @issue_status.update_attribute(:default_done_ratio, 50)
768 @issue_status.update_attribute(:default_done_ratio, 50)
765 @issue2 = Issue.find(2)
769 @issue2 = Issue.find(2)
766 @issue_status2 = IssueStatus.find(2)
770 @issue_status2 = IssueStatus.find(2)
767 @issue_status2.update_attribute(:default_done_ratio, 0)
771 @issue_status2.update_attribute(:default_done_ratio, 0)
768 end
772 end
769
773
770 context "with Setting.issue_done_ratio using the issue_field" do
774 context "with Setting.issue_done_ratio using the issue_field" do
771 setup do
775 setup do
772 Setting.issue_done_ratio = 'issue_field'
776 Setting.issue_done_ratio = 'issue_field'
773 end
777 end
774
778
775 should "read the issue's field" do
779 should "read the issue's field" do
776 assert_equal 0, @issue.done_ratio
780 assert_equal 0, @issue.done_ratio
777 assert_equal 30, @issue2.done_ratio
781 assert_equal 30, @issue2.done_ratio
778 end
782 end
779 end
783 end
780
784
781 context "with Setting.issue_done_ratio using the issue_status" do
785 context "with Setting.issue_done_ratio using the issue_status" do
782 setup do
786 setup do
783 Setting.issue_done_ratio = 'issue_status'
787 Setting.issue_done_ratio = 'issue_status'
784 end
788 end
785
789
786 should "read the Issue Status's default done ratio" do
790 should "read the Issue Status's default done ratio" do
787 assert_equal 50, @issue.done_ratio
791 assert_equal 50, @issue.done_ratio
788 assert_equal 0, @issue2.done_ratio
792 assert_equal 0, @issue2.done_ratio
789 end
793 end
790 end
794 end
791 end
795 end
792
796
793 context "#update_done_ratio_from_issue_status" do
797 context "#update_done_ratio_from_issue_status" do
794 setup do
798 setup do
795 @issue = Issue.find(1)
799 @issue = Issue.find(1)
796 @issue_status = IssueStatus.find(1)
800 @issue_status = IssueStatus.find(1)
797 @issue_status.update_attribute(:default_done_ratio, 50)
801 @issue_status.update_attribute(:default_done_ratio, 50)
798 @issue2 = Issue.find(2)
802 @issue2 = Issue.find(2)
799 @issue_status2 = IssueStatus.find(2)
803 @issue_status2 = IssueStatus.find(2)
800 @issue_status2.update_attribute(:default_done_ratio, 0)
804 @issue_status2.update_attribute(:default_done_ratio, 0)
801 end
805 end
802
806
803 context "with Setting.issue_done_ratio using the issue_field" do
807 context "with Setting.issue_done_ratio using the issue_field" do
804 setup do
808 setup do
805 Setting.issue_done_ratio = 'issue_field'
809 Setting.issue_done_ratio = 'issue_field'
806 end
810 end
807
811
808 should "not change the issue" do
812 should "not change the issue" do
809 @issue.update_done_ratio_from_issue_status
813 @issue.update_done_ratio_from_issue_status
810 @issue2.update_done_ratio_from_issue_status
814 @issue2.update_done_ratio_from_issue_status
811
815
812 assert_equal 0, @issue.read_attribute(:done_ratio)
816 assert_equal 0, @issue.read_attribute(:done_ratio)
813 assert_equal 30, @issue2.read_attribute(:done_ratio)
817 assert_equal 30, @issue2.read_attribute(:done_ratio)
814 end
818 end
815 end
819 end
816
820
817 context "with Setting.issue_done_ratio using the issue_status" do
821 context "with Setting.issue_done_ratio using the issue_status" do
818 setup do
822 setup do
819 Setting.issue_done_ratio = 'issue_status'
823 Setting.issue_done_ratio = 'issue_status'
820 end
824 end
821
825
822 should "change the issue's done ratio" do
826 should "change the issue's done ratio" do
823 @issue.update_done_ratio_from_issue_status
827 @issue.update_done_ratio_from_issue_status
824 @issue2.update_done_ratio_from_issue_status
828 @issue2.update_done_ratio_from_issue_status
825
829
826 assert_equal 50, @issue.read_attribute(:done_ratio)
830 assert_equal 50, @issue.read_attribute(:done_ratio)
827 assert_equal 0, @issue2.read_attribute(:done_ratio)
831 assert_equal 0, @issue2.read_attribute(:done_ratio)
828 end
832 end
829 end
833 end
830 end
834 end
831
835
832 test "#by_tracker" do
836 test "#by_tracker" do
833 User.current = User.anonymous
837 User.current = User.anonymous
834 groups = Issue.by_tracker(Project.find(1))
838 groups = Issue.by_tracker(Project.find(1))
835 assert_equal 3, groups.size
839 assert_equal 3, groups.size
836 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
840 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
837 end
841 end
838
842
839 test "#by_version" do
843 test "#by_version" do
840 User.current = User.anonymous
844 User.current = User.anonymous
841 groups = Issue.by_version(Project.find(1))
845 groups = Issue.by_version(Project.find(1))
842 assert_equal 3, groups.size
846 assert_equal 3, groups.size
843 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
847 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
844 end
848 end
845
849
846 test "#by_priority" do
850 test "#by_priority" do
847 User.current = User.anonymous
851 User.current = User.anonymous
848 groups = Issue.by_priority(Project.find(1))
852 groups = Issue.by_priority(Project.find(1))
849 assert_equal 4, groups.size
853 assert_equal 4, groups.size
850 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
854 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
851 end
855 end
852
856
853 test "#by_category" do
857 test "#by_category" do
854 User.current = User.anonymous
858 User.current = User.anonymous
855 groups = Issue.by_category(Project.find(1))
859 groups = Issue.by_category(Project.find(1))
856 assert_equal 2, groups.size
860 assert_equal 2, groups.size
857 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
861 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
858 end
862 end
859
863
860 test "#by_assigned_to" do
864 test "#by_assigned_to" do
861 User.current = User.anonymous
865 User.current = User.anonymous
862 groups = Issue.by_assigned_to(Project.find(1))
866 groups = Issue.by_assigned_to(Project.find(1))
863 assert_equal 2, groups.size
867 assert_equal 2, groups.size
864 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
868 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
865 end
869 end
866
870
867 test "#by_author" do
871 test "#by_author" do
868 User.current = User.anonymous
872 User.current = User.anonymous
869 groups = Issue.by_author(Project.find(1))
873 groups = Issue.by_author(Project.find(1))
870 assert_equal 4, groups.size
874 assert_equal 4, groups.size
871 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
875 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
872 end
876 end
873
877
874 test "#by_subproject" do
878 test "#by_subproject" do
875 User.current = User.anonymous
879 User.current = User.anonymous
876 groups = Issue.by_subproject(Project.find(1))
880 groups = Issue.by_subproject(Project.find(1))
877 # Private descendant not visible
881 # Private descendant not visible
878 assert_equal 1, groups.size
882 assert_equal 1, groups.size
879 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
883 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
880 end
884 end
881
885
882
886
883 context ".allowed_target_projects_on_move" do
887 context ".allowed_target_projects_on_move" do
884 should "return all active projects for admin users" do
888 should "return all active projects for admin users" do
885 User.current = User.find(1)
889 User.current = User.find(1)
886 assert_equal Project.active.count, Issue.allowed_target_projects_on_move.size
890 assert_equal Project.active.count, Issue.allowed_target_projects_on_move.size
887 end
891 end
888
892
889 should "return allowed projects for non admin users" do
893 should "return allowed projects for non admin users" do
890 User.current = User.find(2)
894 User.current = User.find(2)
891 Role.non_member.remove_permission! :move_issues
895 Role.non_member.remove_permission! :move_issues
892 assert_equal 3, Issue.allowed_target_projects_on_move.size
896 assert_equal 3, Issue.allowed_target_projects_on_move.size
893
897
894 Role.non_member.add_permission! :move_issues
898 Role.non_member.add_permission! :move_issues
895 assert_equal Project.active.count, Issue.allowed_target_projects_on_move.size
899 assert_equal Project.active.count, Issue.allowed_target_projects_on_move.size
896 end
900 end
897 end
901 end
898
902
899 def test_recently_updated_with_limit_scopes
903 def test_recently_updated_with_limit_scopes
900 #should return the last updated issue
904 #should return the last updated issue
901 assert_equal 1, Issue.recently_updated.with_limit(1).length
905 assert_equal 1, Issue.recently_updated.with_limit(1).length
902 assert_equal Issue.find(:first, :order => "updated_on DESC"), Issue.recently_updated.with_limit(1).first
906 assert_equal Issue.find(:first, :order => "updated_on DESC"), Issue.recently_updated.with_limit(1).first
903 end
907 end
904
908
905 def test_on_active_projects_scope
909 def test_on_active_projects_scope
906 assert Project.find(2).archive
910 assert Project.find(2).archive
907
911
908 before = Issue.on_active_project.length
912 before = Issue.on_active_project.length
909 # test inclusion to results
913 # test inclusion to results
910 issue = Issue.generate_for_project!(Project.find(1), :tracker => Project.find(2).trackers.first)
914 issue = Issue.generate_for_project!(Project.find(1), :tracker => Project.find(2).trackers.first)
911 assert_equal before + 1, Issue.on_active_project.length
915 assert_equal before + 1, Issue.on_active_project.length
912
916
913 # Move to an archived project
917 # Move to an archived project
914 issue.project = Project.find(2)
918 issue.project = Project.find(2)
915 assert issue.save
919 assert issue.save
916 assert_equal before, Issue.on_active_project.length
920 assert_equal before, Issue.on_active_project.length
917 end
921 end
918
922
919 context "Issue#recipients" do
923 context "Issue#recipients" do
920 setup do
924 setup do
921 @project = Project.find(1)
925 @project = Project.find(1)
922 @author = User.generate_with_protected!
926 @author = User.generate_with_protected!
923 @assignee = User.generate_with_protected!
927 @assignee = User.generate_with_protected!
924 @issue = Issue.generate_for_project!(@project, :assigned_to => @assignee, :author => @author)
928 @issue = Issue.generate_for_project!(@project, :assigned_to => @assignee, :author => @author)
925 end
929 end
926
930
927 should "include project recipients" do
931 should "include project recipients" do
928 assert @project.recipients.present?
932 assert @project.recipients.present?
929 @project.recipients.each do |project_recipient|
933 @project.recipients.each do |project_recipient|
930 assert @issue.recipients.include?(project_recipient)
934 assert @issue.recipients.include?(project_recipient)
931 end
935 end
932 end
936 end
933
937
934 should "include the author if the author is active" do
938 should "include the author if the author is active" do
935 assert @issue.author, "No author set for Issue"
939 assert @issue.author, "No author set for Issue"
936 assert @issue.recipients.include?(@issue.author.mail)
940 assert @issue.recipients.include?(@issue.author.mail)
937 end
941 end
938
942
939 should "include the assigned to user if the assigned to user is active" do
943 should "include the assigned to user if the assigned to user is active" do
940 assert @issue.assigned_to, "No assigned_to set for Issue"
944 assert @issue.assigned_to, "No assigned_to set for Issue"
941 assert @issue.recipients.include?(@issue.assigned_to.mail)
945 assert @issue.recipients.include?(@issue.assigned_to.mail)
942 end
946 end
943
947
944 should "not include users who opt out of all email" do
948 should "not include users who opt out of all email" do
945 @author.update_attribute(:mail_notification, :none)
949 @author.update_attribute(:mail_notification, :none)
946
950
947 assert !@issue.recipients.include?(@issue.author.mail)
951 assert !@issue.recipients.include?(@issue.author.mail)
948 end
952 end
949
953
950 should "not include the issue author if they are only notified of assigned issues" do
954 should "not include the issue author if they are only notified of assigned issues" do
951 @author.update_attribute(:mail_notification, :only_assigned)
955 @author.update_attribute(:mail_notification, :only_assigned)
952
956
953 assert !@issue.recipients.include?(@issue.author.mail)
957 assert !@issue.recipients.include?(@issue.author.mail)
954 end
958 end
955
959
956 should "not include the assigned user if they are only notified of owned issues" do
960 should "not include the assigned user if they are only notified of owned issues" do
957 @assignee.update_attribute(:mail_notification, :only_owner)
961 @assignee.update_attribute(:mail_notification, :only_owner)
958
962
959 assert !@issue.recipients.include?(@issue.assigned_to.mail)
963 assert !@issue.recipients.include?(@issue.assigned_to.mail)
960 end
964 end
961
965
962 end
966 end
963 end
967 end
@@ -1,553 +1,553
1 module CollectiveIdea #:nodoc:
1 module CollectiveIdea #:nodoc:
2 module Acts #:nodoc:
2 module Acts #:nodoc:
3 module NestedSet #:nodoc:
3 module NestedSet #:nodoc:
4 def self.included(base)
4 def self.included(base)
5 base.extend(SingletonMethods)
5 base.extend(SingletonMethods)
6 end
6 end
7
7
8 # This acts provides Nested Set functionality. Nested Set is a smart way to implement
8 # This acts provides Nested Set functionality. Nested Set is a smart way to implement
9 # an _ordered_ tree, with the added feature that you can select the children and all of their
9 # an _ordered_ tree, with the added feature that you can select the children and all of their
10 # descendants with a single query. The drawback is that insertion or move need some complex
10 # descendants with a single query. The drawback is that insertion or move need some complex
11 # sql queries. But everything is done here by this module!
11 # sql queries. But everything is done here by this module!
12 #
12 #
13 # Nested sets are appropriate each time you want either an orderd tree (menus,
13 # Nested sets are appropriate each time you want either an orderd tree (menus,
14 # commercial categories) or an efficient way of querying big trees (threaded posts).
14 # commercial categories) or an efficient way of querying big trees (threaded posts).
15 #
15 #
16 # == API
16 # == API
17 #
17 #
18 # Methods names are aligned with acts_as_tree as much as possible, to make replacment from one
18 # Methods names are aligned with acts_as_tree as much as possible, to make replacment from one
19 # by another easier, except for the creation:
19 # by another easier, except for the creation:
20 #
20 #
21 # in acts_as_tree:
21 # in acts_as_tree:
22 # item.children.create(:name => "child1")
22 # item.children.create(:name => "child1")
23 #
23 #
24 # in acts_as_nested_set:
24 # in acts_as_nested_set:
25 # # adds a new item at the "end" of the tree, i.e. with child.left = max(tree.right)+1
25 # # adds a new item at the "end" of the tree, i.e. with child.left = max(tree.right)+1
26 # child = MyClass.new(:name => "child1")
26 # child = MyClass.new(:name => "child1")
27 # child.save
27 # child.save
28 # # now move the item to its right place
28 # # now move the item to its right place
29 # child.move_to_child_of my_item
29 # child.move_to_child_of my_item
30 #
30 #
31 # You can pass an id or an object to:
31 # You can pass an id or an object to:
32 # * <tt>#move_to_child_of</tt>
32 # * <tt>#move_to_child_of</tt>
33 # * <tt>#move_to_right_of</tt>
33 # * <tt>#move_to_right_of</tt>
34 # * <tt>#move_to_left_of</tt>
34 # * <tt>#move_to_left_of</tt>
35 #
35 #
36 module SingletonMethods
36 module SingletonMethods
37 # Configuration options are:
37 # Configuration options are:
38 #
38 #
39 # * +:parent_column+ - specifies the column name to use for keeping the position integer (default: parent_id)
39 # * +:parent_column+ - specifies the column name to use for keeping the position integer (default: parent_id)
40 # * +:left_column+ - column name for left boundry data, default "lft"
40 # * +:left_column+ - column name for left boundry data, default "lft"
41 # * +:right_column+ - column name for right boundry data, default "rgt"
41 # * +:right_column+ - column name for right boundry data, default "rgt"
42 # * +:scope+ - restricts what is to be considered a list. Given a symbol, it'll attach "_id"
42 # * +:scope+ - restricts what is to be considered a list. Given a symbol, it'll attach "_id"
43 # (if it hasn't been already) and use that as the foreign key restriction. You
43 # (if it hasn't been already) and use that as the foreign key restriction. You
44 # can also pass an array to scope by multiple attributes.
44 # can also pass an array to scope by multiple attributes.
45 # Example: <tt>acts_as_nested_set :scope => [:notable_id, :notable_type]</tt>
45 # Example: <tt>acts_as_nested_set :scope => [:notable_id, :notable_type]</tt>
46 # * +:dependent+ - behavior for cascading destroy. If set to :destroy, all the
46 # * +:dependent+ - behavior for cascading destroy. If set to :destroy, all the
47 # child objects are destroyed alongside this object by calling their destroy
47 # child objects are destroyed alongside this object by calling their destroy
48 # method. If set to :delete_all (default), all the child objects are deleted
48 # method. If set to :delete_all (default), all the child objects are deleted
49 # without calling their destroy method.
49 # without calling their destroy method.
50 #
50 #
51 # See CollectiveIdea::Acts::NestedSet::ClassMethods for a list of class methods and
51 # See CollectiveIdea::Acts::NestedSet::ClassMethods for a list of class methods and
52 # CollectiveIdea::Acts::NestedSet::InstanceMethods for a list of instance methods added
52 # CollectiveIdea::Acts::NestedSet::InstanceMethods for a list of instance methods added
53 # to acts_as_nested_set models
53 # to acts_as_nested_set models
54 def acts_as_nested_set(options = {})
54 def acts_as_nested_set(options = {})
55 options = {
55 options = {
56 :parent_column => 'parent_id',
56 :parent_column => 'parent_id',
57 :left_column => 'lft',
57 :left_column => 'lft',
58 :right_column => 'rgt',
58 :right_column => 'rgt',
59 :order => 'id',
59 :order => 'id',
60 :dependent => :delete_all, # or :destroy
60 :dependent => :delete_all, # or :destroy
61 }.merge(options)
61 }.merge(options)
62
62
63 if options[:scope].is_a?(Symbol) && options[:scope].to_s !~ /_id$/
63 if options[:scope].is_a?(Symbol) && options[:scope].to_s !~ /_id$/
64 options[:scope] = "#{options[:scope]}_id".intern
64 options[:scope] = "#{options[:scope]}_id".intern
65 end
65 end
66
66
67 write_inheritable_attribute :acts_as_nested_set_options, options
67 write_inheritable_attribute :acts_as_nested_set_options, options
68 class_inheritable_reader :acts_as_nested_set_options
68 class_inheritable_reader :acts_as_nested_set_options
69
69
70 include Comparable
70 include Comparable
71 include Columns
71 include Columns
72 include InstanceMethods
72 include InstanceMethods
73 extend Columns
73 extend Columns
74 extend ClassMethods
74 extend ClassMethods
75
75
76 # no bulk assignment
76 # no bulk assignment
77 attr_protected left_column_name.intern,
77 attr_protected left_column_name.intern,
78 right_column_name.intern,
78 right_column_name.intern,
79 parent_column_name.intern
79 parent_column_name.intern
80
80
81 before_create :set_default_left_and_right
81 before_create :set_default_left_and_right
82 before_destroy :prune_from_tree
82 before_destroy :prune_from_tree
83
83
84 # no assignment to structure fields
84 # no assignment to structure fields
85 [left_column_name, right_column_name, parent_column_name].each do |column|
85 [left_column_name, right_column_name, parent_column_name].each do |column|
86 module_eval <<-"end_eval", __FILE__, __LINE__
86 module_eval <<-"end_eval", __FILE__, __LINE__
87 def #{column}=(x)
87 def #{column}=(x)
88 raise ActiveRecord::ActiveRecordError, "Unauthorized assignment to #{column}: it's an internal field handled by acts_as_nested_set code, use move_to_* methods instead."
88 raise ActiveRecord::ActiveRecordError, "Unauthorized assignment to #{column}: it's an internal field handled by acts_as_nested_set code, use move_to_* methods instead."
89 end
89 end
90 end_eval
90 end_eval
91 end
91 end
92
92
93 named_scope :roots, :conditions => {parent_column_name => nil}, :order => quoted_left_column_name
93 named_scope :roots, :conditions => {parent_column_name => nil}, :order => quoted_left_column_name
94 named_scope :leaves, :conditions => "#{quoted_right_column_name} - #{quoted_left_column_name} = 1", :order => quoted_left_column_name
94 named_scope :leaves, :conditions => "#{quoted_right_column_name} - #{quoted_left_column_name} = 1", :order => quoted_left_column_name
95 if self.respond_to?(:define_callbacks)
95 if self.respond_to?(:define_callbacks)
96 define_callbacks("before_move", "after_move")
96 define_callbacks("before_move", "after_move")
97 end
97 end
98
98
99
99
100 end
100 end
101
101
102 end
102 end
103
103
104 module ClassMethods
104 module ClassMethods
105
105
106 # Returns the first root
106 # Returns the first root
107 def root
107 def root
108 roots.find(:first)
108 roots.find(:first)
109 end
109 end
110
110
111 def valid?
111 def valid?
112 left_and_rights_valid? && no_duplicates_for_columns? && all_roots_valid?
112 left_and_rights_valid? && no_duplicates_for_columns? && all_roots_valid?
113 end
113 end
114
114
115 def left_and_rights_valid?
115 def left_and_rights_valid?
116 count(
116 count(
117 :joins => "LEFT OUTER JOIN #{quoted_table_name} AS parent ON " +
117 :joins => "LEFT OUTER JOIN #{quoted_table_name} AS parent ON " +
118 "#{quoted_table_name}.#{quoted_parent_column_name} = parent.#{primary_key}",
118 "#{quoted_table_name}.#{quoted_parent_column_name} = parent.#{primary_key}",
119 :conditions =>
119 :conditions =>
120 "#{quoted_table_name}.#{quoted_left_column_name} IS NULL OR " +
120 "#{quoted_table_name}.#{quoted_left_column_name} IS NULL OR " +
121 "#{quoted_table_name}.#{quoted_right_column_name} IS NULL OR " +
121 "#{quoted_table_name}.#{quoted_right_column_name} IS NULL OR " +
122 "#{quoted_table_name}.#{quoted_left_column_name} >= " +
122 "#{quoted_table_name}.#{quoted_left_column_name} >= " +
123 "#{quoted_table_name}.#{quoted_right_column_name} OR " +
123 "#{quoted_table_name}.#{quoted_right_column_name} OR " +
124 "(#{quoted_table_name}.#{quoted_parent_column_name} IS NOT NULL AND " +
124 "(#{quoted_table_name}.#{quoted_parent_column_name} IS NOT NULL AND " +
125 "(#{quoted_table_name}.#{quoted_left_column_name} <= parent.#{quoted_left_column_name} OR " +
125 "(#{quoted_table_name}.#{quoted_left_column_name} <= parent.#{quoted_left_column_name} OR " +
126 "#{quoted_table_name}.#{quoted_right_column_name} >= parent.#{quoted_right_column_name}))"
126 "#{quoted_table_name}.#{quoted_right_column_name} >= parent.#{quoted_right_column_name}))"
127 ) == 0
127 ) == 0
128 end
128 end
129
129
130 def no_duplicates_for_columns?
130 def no_duplicates_for_columns?
131 scope_string = Array(acts_as_nested_set_options[:scope]).map do |c|
131 scope_string = Array(acts_as_nested_set_options[:scope]).map do |c|
132 connection.quote_column_name(c)
132 connection.quote_column_name(c)
133 end.push(nil).join(", ")
133 end.push(nil).join(", ")
134 [quoted_left_column_name, quoted_right_column_name].all? do |column|
134 [quoted_left_column_name, quoted_right_column_name].all? do |column|
135 # No duplicates
135 # No duplicates
136 find(:first,
136 find(:first,
137 :select => "#{scope_string}#{column}, COUNT(#{column})",
137 :select => "#{scope_string}#{column}, COUNT(#{column})",
138 :group => "#{scope_string}#{column}
138 :group => "#{scope_string}#{column}
139 HAVING COUNT(#{column}) > 1").nil?
139 HAVING COUNT(#{column}) > 1").nil?
140 end
140 end
141 end
141 end
142
142
143 # Wrapper for each_root_valid? that can deal with scope.
143 # Wrapper for each_root_valid? that can deal with scope.
144 def all_roots_valid?
144 def all_roots_valid?
145 if acts_as_nested_set_options[:scope]
145 if acts_as_nested_set_options[:scope]
146 roots(:group => scope_column_names).group_by{|record| scope_column_names.collect{|col| record.send(col.to_sym)}}.all? do |scope, grouped_roots|
146 roots(:group => scope_column_names).group_by{|record| scope_column_names.collect{|col| record.send(col.to_sym)}}.all? do |scope, grouped_roots|
147 each_root_valid?(grouped_roots)
147 each_root_valid?(grouped_roots)
148 end
148 end
149 else
149 else
150 each_root_valid?(roots)
150 each_root_valid?(roots)
151 end
151 end
152 end
152 end
153
153
154 def each_root_valid?(roots_to_validate)
154 def each_root_valid?(roots_to_validate)
155 left = right = 0
155 left = right = 0
156 roots_to_validate.all? do |root|
156 roots_to_validate.all? do |root|
157 (root.left > left && root.right > right).tap do
157 (root.left > left && root.right > right).tap do
158 left = root.left
158 left = root.left
159 right = root.right
159 right = root.right
160 end
160 end
161 end
161 end
162 end
162 end
163
163
164 # Rebuilds the left & rights if unset or invalid. Also very useful for converting from acts_as_tree.
164 # Rebuilds the left & rights if unset or invalid. Also very useful for converting from acts_as_tree.
165 def rebuild!
165 def rebuild!
166 # Don't rebuild a valid tree.
166 # Don't rebuild a valid tree.
167 return true if valid?
167 return true if valid?
168
168
169 scope = lambda{|node|}
169 scope = lambda{|node|}
170 if acts_as_nested_set_options[:scope]
170 if acts_as_nested_set_options[:scope]
171 scope = lambda{|node|
171 scope = lambda{|node|
172 scope_column_names.inject(""){|str, column_name|
172 scope_column_names.inject(""){|str, column_name|
173 str << "AND #{connection.quote_column_name(column_name)} = #{connection.quote(node.send(column_name.to_sym))} "
173 str << "AND #{connection.quote_column_name(column_name)} = #{connection.quote(node.send(column_name.to_sym))} "
174 }
174 }
175 }
175 }
176 end
176 end
177 indices = {}
177 indices = {}
178
178
179 set_left_and_rights = lambda do |node|
179 set_left_and_rights = lambda do |node|
180 # set left
180 # set left
181 node[left_column_name] = indices[scope.call(node)] += 1
181 node[left_column_name] = indices[scope.call(node)] += 1
182 # find
182 # find
183 find(:all, :conditions => ["#{quoted_parent_column_name} = ? #{scope.call(node)}", node], :order => "#{quoted_left_column_name}, #{quoted_right_column_name}, #{acts_as_nested_set_options[:order]}").each{|n| set_left_and_rights.call(n) }
183 find(:all, :conditions => ["#{quoted_parent_column_name} = ? #{scope.call(node)}", node], :order => "#{quoted_left_column_name}, #{quoted_right_column_name}, #{acts_as_nested_set_options[:order]}").each{|n| set_left_and_rights.call(n) }
184 # set right
184 # set right
185 node[right_column_name] = indices[scope.call(node)] += 1
185 node[right_column_name] = indices[scope.call(node)] += 1
186 node.save!
186 node.save!
187 end
187 end
188
188
189 # Find root node(s)
189 # Find root node(s)
190 root_nodes = find(:all, :conditions => "#{quoted_parent_column_name} IS NULL", :order => "#{quoted_left_column_name}, #{quoted_right_column_name}, #{acts_as_nested_set_options[:order]}").each do |root_node|
190 root_nodes = find(:all, :conditions => "#{quoted_parent_column_name} IS NULL", :order => "#{quoted_left_column_name}, #{quoted_right_column_name}, #{acts_as_nested_set_options[:order]}").each do |root_node|
191 # setup index for this scope
191 # setup index for this scope
192 indices[scope.call(root_node)] ||= 0
192 indices[scope.call(root_node)] ||= 0
193 set_left_and_rights.call(root_node)
193 set_left_and_rights.call(root_node)
194 end
194 end
195 end
195 end
196 end
196 end
197
197
198 # Mixed into both classes and instances to provide easy access to the column names
198 # Mixed into both classes and instances to provide easy access to the column names
199 module Columns
199 module Columns
200 def left_column_name
200 def left_column_name
201 acts_as_nested_set_options[:left_column]
201 acts_as_nested_set_options[:left_column]
202 end
202 end
203
203
204 def right_column_name
204 def right_column_name
205 acts_as_nested_set_options[:right_column]
205 acts_as_nested_set_options[:right_column]
206 end
206 end
207
207
208 def parent_column_name
208 def parent_column_name
209 acts_as_nested_set_options[:parent_column]
209 acts_as_nested_set_options[:parent_column]
210 end
210 end
211
211
212 def scope_column_names
212 def scope_column_names
213 Array(acts_as_nested_set_options[:scope])
213 Array(acts_as_nested_set_options[:scope])
214 end
214 end
215
215
216 def quoted_left_column_name
216 def quoted_left_column_name
217 connection.quote_column_name(left_column_name)
217 connection.quote_column_name(left_column_name)
218 end
218 end
219
219
220 def quoted_right_column_name
220 def quoted_right_column_name
221 connection.quote_column_name(right_column_name)
221 connection.quote_column_name(right_column_name)
222 end
222 end
223
223
224 def quoted_parent_column_name
224 def quoted_parent_column_name
225 connection.quote_column_name(parent_column_name)
225 connection.quote_column_name(parent_column_name)
226 end
226 end
227
227
228 def quoted_scope_column_names
228 def quoted_scope_column_names
229 scope_column_names.collect {|column_name| connection.quote_column_name(column_name) }
229 scope_column_names.collect {|column_name| connection.quote_column_name(column_name) }
230 end
230 end
231 end
231 end
232
232
233 # Any instance method that returns a collection makes use of Rails 2.1's named_scope (which is bundled for Rails 2.0), so it can be treated as a finder.
233 # Any instance method that returns a collection makes use of Rails 2.1's named_scope (which is bundled for Rails 2.0), so it can be treated as a finder.
234 #
234 #
235 # category.self_and_descendants.count
235 # category.self_and_descendants.count
236 # category.ancestors.find(:all, :conditions => "name like '%foo%'")
236 # category.ancestors.find(:all, :conditions => "name like '%foo%'")
237 module InstanceMethods
237 module InstanceMethods
238 # Value of the parent column
238 # Value of the parent column
239 def parent_id
239 def parent_id
240 self[parent_column_name]
240 self[parent_column_name]
241 end
241 end
242
242
243 # Value of the left column
243 # Value of the left column
244 def left
244 def left
245 self[left_column_name]
245 self[left_column_name]
246 end
246 end
247
247
248 # Value of the right column
248 # Value of the right column
249 def right
249 def right
250 self[right_column_name]
250 self[right_column_name]
251 end
251 end
252
252
253 # Returns true if this is a root node.
253 # Returns true if this is a root node.
254 def root?
254 def root?
255 parent_id.nil?
255 parent_id.nil?
256 end
256 end
257
257
258 def leaf?
258 def leaf?
259 new_record? || (right - left == 1)
259 new_record? || (right - left == 1)
260 end
260 end
261
261
262 # Returns true is this is a child node
262 # Returns true is this is a child node
263 def child?
263 def child?
264 !parent_id.nil?
264 !parent_id.nil?
265 end
265 end
266
266
267 # order by left column
267 # order by left column
268 def <=>(x)
268 def <=>(x)
269 left <=> x.left
269 left <=> x.left
270 end
270 end
271
271
272 # Redefine to act like active record
272 # Redefine to act like active record
273 def ==(comparison_object)
273 def ==(comparison_object)
274 comparison_object.equal?(self) ||
274 comparison_object.equal?(self) ||
275 (comparison_object.instance_of?(self.class) &&
275 (comparison_object.instance_of?(self.class) &&
276 comparison_object.id == id &&
276 comparison_object.id == id &&
277 !comparison_object.new_record?)
277 !comparison_object.new_record?)
278 end
278 end
279
279
280 # Returns root
280 # Returns root
281 def root
281 def root
282 self_and_ancestors.find(:first)
282 self_and_ancestors.find(:first)
283 end
283 end
284
284
285 # Returns the immediate parent
285 # Returns the immediate parent
286 def parent
286 def parent
287 nested_set_scope.find_by_id(parent_id) if parent_id
287 nested_set_scope.find_by_id(parent_id) if parent_id
288 end
288 end
289
289
290 # Returns the array of all parents and self
290 # Returns the array of all parents and self
291 def self_and_ancestors
291 def self_and_ancestors
292 nested_set_scope.scoped :conditions => [
292 nested_set_scope.scoped :conditions => [
293 "#{self.class.table_name}.#{quoted_left_column_name} <= ? AND #{self.class.table_name}.#{quoted_right_column_name} >= ?", left, right
293 "#{self.class.table_name}.#{quoted_left_column_name} <= ? AND #{self.class.table_name}.#{quoted_right_column_name} >= ?", left, right
294 ]
294 ]
295 end
295 end
296
296
297 # Returns an array of all parents
297 # Returns an array of all parents
298 def ancestors
298 def ancestors
299 without_self self_and_ancestors
299 without_self self_and_ancestors
300 end
300 end
301
301
302 # Returns the array of all children of the parent, including self
302 # Returns the array of all children of the parent, including self
303 def self_and_siblings
303 def self_and_siblings
304 nested_set_scope.scoped :conditions => {parent_column_name => parent_id}
304 nested_set_scope.scoped :conditions => {parent_column_name => parent_id}
305 end
305 end
306
306
307 # Returns the array of all children of the parent, except self
307 # Returns the array of all children of the parent, except self
308 def siblings
308 def siblings
309 without_self self_and_siblings
309 without_self self_and_siblings
310 end
310 end
311
311
312 # Returns a set of all of its nested children which do not have children
312 # Returns a set of all of its nested children which do not have children
313 def leaves
313 def leaves
314 descendants.scoped :conditions => "#{self.class.table_name}.#{quoted_right_column_name} - #{self.class.table_name}.#{quoted_left_column_name} = 1"
314 descendants.scoped :conditions => "#{self.class.table_name}.#{quoted_right_column_name} - #{self.class.table_name}.#{quoted_left_column_name} = 1"
315 end
315 end
316
316
317 # Returns the level of this object in the tree
317 # Returns the level of this object in the tree
318 # root level is 0
318 # root level is 0
319 def level
319 def level
320 parent_id.nil? ? 0 : ancestors.count
320 parent_id.nil? ? 0 : ancestors.count
321 end
321 end
322
322
323 # Returns a set of itself and all of its nested children
323 # Returns a set of itself and all of its nested children
324 def self_and_descendants
324 def self_and_descendants
325 nested_set_scope.scoped :conditions => [
325 nested_set_scope.scoped :conditions => [
326 "#{self.class.table_name}.#{quoted_left_column_name} >= ? AND #{self.class.table_name}.#{quoted_right_column_name} <= ?", left, right
326 "#{self.class.table_name}.#{quoted_left_column_name} >= ? AND #{self.class.table_name}.#{quoted_right_column_name} <= ?", left, right
327 ]
327 ]
328 end
328 end
329
329
330 # Returns a set of all of its children and nested children
330 # Returns a set of all of its children and nested children
331 def descendants
331 def descendants
332 without_self self_and_descendants
332 without_self self_and_descendants
333 end
333 end
334
334
335 # Returns a set of only this entry's immediate children
335 # Returns a set of only this entry's immediate children
336 def children
336 def children
337 nested_set_scope.scoped :conditions => {parent_column_name => self}
337 nested_set_scope.scoped :conditions => {parent_column_name => self}
338 end
338 end
339
339
340 def is_descendant_of?(other)
340 def is_descendant_of?(other)
341 other.left < self.left && self.left < other.right && same_scope?(other)
341 other.left < self.left && self.left < other.right && same_scope?(other)
342 end
342 end
343
343
344 def is_or_is_descendant_of?(other)
344 def is_or_is_descendant_of?(other)
345 other.left <= self.left && self.left < other.right && same_scope?(other)
345 other.left <= self.left && self.left < other.right && same_scope?(other)
346 end
346 end
347
347
348 def is_ancestor_of?(other)
348 def is_ancestor_of?(other)
349 self.left < other.left && other.left < self.right && same_scope?(other)
349 self.left < other.left && other.left < self.right && same_scope?(other)
350 end
350 end
351
351
352 def is_or_is_ancestor_of?(other)
352 def is_or_is_ancestor_of?(other)
353 self.left <= other.left && other.left < self.right && same_scope?(other)
353 self.left <= other.left && other.left < self.right && same_scope?(other)
354 end
354 end
355
355
356 # Check if other model is in the same scope
356 # Check if other model is in the same scope
357 def same_scope?(other)
357 def same_scope?(other)
358 Array(acts_as_nested_set_options[:scope]).all? do |attr|
358 Array(acts_as_nested_set_options[:scope]).all? do |attr|
359 self.send(attr) == other.send(attr)
359 self.send(attr) == other.send(attr)
360 end
360 end
361 end
361 end
362
362
363 # Find the first sibling to the left
363 # Find the first sibling to the left
364 def left_sibling
364 def left_sibling
365 siblings.find(:first, :conditions => ["#{self.class.table_name}.#{quoted_left_column_name} < ?", left],
365 siblings.find(:first, :conditions => ["#{self.class.table_name}.#{quoted_left_column_name} < ?", left],
366 :order => "#{self.class.table_name}.#{quoted_left_column_name} DESC")
366 :order => "#{self.class.table_name}.#{quoted_left_column_name} DESC")
367 end
367 end
368
368
369 # Find the first sibling to the right
369 # Find the first sibling to the right
370 def right_sibling
370 def right_sibling
371 siblings.find(:first, :conditions => ["#{self.class.table_name}.#{quoted_left_column_name} > ?", left])
371 siblings.find(:first, :conditions => ["#{self.class.table_name}.#{quoted_left_column_name} > ?", left])
372 end
372 end
373
373
374 # Shorthand method for finding the left sibling and moving to the left of it.
374 # Shorthand method for finding the left sibling and moving to the left of it.
375 def move_left
375 def move_left
376 move_to_left_of left_sibling
376 move_to_left_of left_sibling
377 end
377 end
378
378
379 # Shorthand method for finding the right sibling and moving to the right of it.
379 # Shorthand method for finding the right sibling and moving to the right of it.
380 def move_right
380 def move_right
381 move_to_right_of right_sibling
381 move_to_right_of right_sibling
382 end
382 end
383
383
384 # Move the node to the left of another node (you can pass id only)
384 # Move the node to the left of another node (you can pass id only)
385 def move_to_left_of(node)
385 def move_to_left_of(node)
386 move_to node, :left
386 move_to node, :left
387 end
387 end
388
388
389 # Move the node to the left of another node (you can pass id only)
389 # Move the node to the left of another node (you can pass id only)
390 def move_to_right_of(node)
390 def move_to_right_of(node)
391 move_to node, :right
391 move_to node, :right
392 end
392 end
393
393
394 # Move the node to the child of another node (you can pass id only)
394 # Move the node to the child of another node (you can pass id only)
395 def move_to_child_of(node)
395 def move_to_child_of(node)
396 move_to node, :child
396 move_to node, :child
397 end
397 end
398
398
399 # Move the node to root nodes
399 # Move the node to root nodes
400 def move_to_root
400 def move_to_root
401 move_to nil, :root
401 move_to nil, :root
402 end
402 end
403
403
404 def move_possible?(target)
404 def move_possible?(target)
405 self != target && # Can't target self
405 self != target && # Can't target self
406 same_scope?(target) && # can't be in different scopes
406 same_scope?(target) && # can't be in different scopes
407 # !(left..right).include?(target.left..target.right) # this needs tested more
407 # !(left..right).include?(target.left..target.right) # this needs tested more
408 # detect impossible move
408 # detect impossible move
409 !((left <= target.left && right >= target.left) or (left <= target.right && right >= target.right))
409 !((left <= target.left && right >= target.left) or (left <= target.right && right >= target.right))
410 end
410 end
411
411
412 def to_text
412 def to_text
413 self_and_descendants.map do |node|
413 self_and_descendants.map do |node|
414 "#{'*'*(node.level+1)} #{node.id} #{node.to_s} (#{node.parent_id}, #{node.left}, #{node.right})"
414 "#{'*'*(node.level+1)} #{node.id} #{node.to_s} (#{node.parent_id}, #{node.left}, #{node.right})"
415 end.join("\n")
415 end.join("\n")
416 end
416 end
417
417
418 protected
418 protected
419
419
420 def without_self(scope)
420 def without_self(scope)
421 scope.scoped :conditions => ["#{self.class.table_name}.#{self.class.primary_key} != ?", self]
421 scope.scoped :conditions => ["#{self.class.table_name}.#{self.class.primary_key} != ?", self]
422 end
422 end
423
423
424 # All nested set queries should use this nested_set_scope, which performs finds on
424 # All nested set queries should use this nested_set_scope, which performs finds on
425 # the base ActiveRecord class, using the :scope declared in the acts_as_nested_set
425 # the base ActiveRecord class, using the :scope declared in the acts_as_nested_set
426 # declaration.
426 # declaration.
427 def nested_set_scope
427 def nested_set_scope
428 options = {:order => quoted_left_column_name}
428 options = {:order => "#{self.class.table_name}.#{quoted_left_column_name}"}
429 scopes = Array(acts_as_nested_set_options[:scope])
429 scopes = Array(acts_as_nested_set_options[:scope])
430 options[:conditions] = scopes.inject({}) do |conditions,attr|
430 options[:conditions] = scopes.inject({}) do |conditions,attr|
431 conditions.merge attr => self[attr]
431 conditions.merge attr => self[attr]
432 end unless scopes.empty?
432 end unless scopes.empty?
433 self.class.base_class.scoped options
433 self.class.base_class.scoped options
434 end
434 end
435
435
436 # on creation, set automatically lft and rgt to the end of the tree
436 # on creation, set automatically lft and rgt to the end of the tree
437 def set_default_left_and_right
437 def set_default_left_and_right
438 maxright = nested_set_scope.maximum(right_column_name) || 0
438 maxright = nested_set_scope.maximum(right_column_name) || 0
439 # adds the new node to the right of all existing nodes
439 # adds the new node to the right of all existing nodes
440 self[left_column_name] = maxright + 1
440 self[left_column_name] = maxright + 1
441 self[right_column_name] = maxright + 2
441 self[right_column_name] = maxright + 2
442 end
442 end
443
443
444 # Prunes a branch off of the tree, shifting all of the elements on the right
444 # Prunes a branch off of the tree, shifting all of the elements on the right
445 # back to the left so the counts still work.
445 # back to the left so the counts still work.
446 def prune_from_tree
446 def prune_from_tree
447 return if right.nil? || left.nil? || leaf? || !self.class.exists?(id)
447 return if right.nil? || left.nil? || leaf? || !self.class.exists?(id)
448
448
449 delete_method = acts_as_nested_set_options[:dependent] == :destroy ?
449 delete_method = acts_as_nested_set_options[:dependent] == :destroy ?
450 :destroy_all : :delete_all
450 :destroy_all : :delete_all
451
451
452 # TODO: should destroy children (not descendants) when deleted_method is :destroy_all
452 # TODO: should destroy children (not descendants) when deleted_method is :destroy_all
453 self.class.base_class.transaction do
453 self.class.base_class.transaction do
454 reload_nested_set
454 reload_nested_set
455 nested_set_scope.send(delete_method,
455 nested_set_scope.send(delete_method,
456 ["#{quoted_left_column_name} > ? AND #{quoted_right_column_name} < ?",
456 ["#{quoted_left_column_name} > ? AND #{quoted_right_column_name} < ?",
457 left, right]
457 left, right]
458 )
458 )
459 reload_nested_set
459 reload_nested_set
460 diff = right - left + 1
460 diff = right - left + 1
461 nested_set_scope.update_all(
461 nested_set_scope.update_all(
462 ["#{quoted_left_column_name} = (#{quoted_left_column_name} - ?)", diff],
462 ["#{quoted_left_column_name} = (#{quoted_left_column_name} - ?)", diff],
463 ["#{quoted_left_column_name} >= ?", right]
463 ["#{quoted_left_column_name} >= ?", right]
464 )
464 )
465 nested_set_scope.update_all(
465 nested_set_scope.update_all(
466 ["#{quoted_right_column_name} = (#{quoted_right_column_name} - ?)", diff],
466 ["#{quoted_right_column_name} = (#{quoted_right_column_name} - ?)", diff],
467 ["#{quoted_right_column_name} >= ?", right]
467 ["#{quoted_right_column_name} >= ?", right]
468 )
468 )
469 end
469 end
470
470
471 # Reload is needed because children may have updated their parent (self) during deletion.
471 # Reload is needed because children may have updated their parent (self) during deletion.
472 reload
472 reload
473 end
473 end
474
474
475 # reload left, right, and parent
475 # reload left, right, and parent
476 def reload_nested_set
476 def reload_nested_set
477 reload(:select => "#{quoted_left_column_name}, " +
477 reload(:select => "#{quoted_left_column_name}, " +
478 "#{quoted_right_column_name}, #{quoted_parent_column_name}")
478 "#{quoted_right_column_name}, #{quoted_parent_column_name}")
479 end
479 end
480
480
481 def move_to(target, position)
481 def move_to(target, position)
482 raise ActiveRecord::ActiveRecordError, "You cannot move a new node" if self.new_record?
482 raise ActiveRecord::ActiveRecordError, "You cannot move a new node" if self.new_record?
483 return if callback(:before_move) == false
483 return if callback(:before_move) == false
484 transaction do
484 transaction do
485 if target.is_a? self.class.base_class
485 if target.is_a? self.class.base_class
486 target.reload_nested_set
486 target.reload_nested_set
487 elsif position != :root
487 elsif position != :root
488 # load object if node is not an object
488 # load object if node is not an object
489 target = nested_set_scope.find(target)
489 target = nested_set_scope.find(target)
490 end
490 end
491 self.reload_nested_set
491 self.reload_nested_set
492
492
493 unless position == :root || move_possible?(target)
493 unless position == :root || move_possible?(target)
494 raise ActiveRecord::ActiveRecordError, "Impossible move, target node cannot be inside moved tree."
494 raise ActiveRecord::ActiveRecordError, "Impossible move, target node cannot be inside moved tree."
495 end
495 end
496
496
497 bound = case position
497 bound = case position
498 when :child; target[right_column_name]
498 when :child; target[right_column_name]
499 when :left; target[left_column_name]
499 when :left; target[left_column_name]
500 when :right; target[right_column_name] + 1
500 when :right; target[right_column_name] + 1
501 when :root; 1
501 when :root; 1
502 else raise ActiveRecord::ActiveRecordError, "Position should be :child, :left, :right or :root ('#{position}' received)."
502 else raise ActiveRecord::ActiveRecordError, "Position should be :child, :left, :right or :root ('#{position}' received)."
503 end
503 end
504
504
505 if bound > self[right_column_name]
505 if bound > self[right_column_name]
506 bound = bound - 1
506 bound = bound - 1
507 other_bound = self[right_column_name] + 1
507 other_bound = self[right_column_name] + 1
508 else
508 else
509 other_bound = self[left_column_name] - 1
509 other_bound = self[left_column_name] - 1
510 end
510 end
511
511
512 # there would be no change
512 # there would be no change
513 return if bound == self[right_column_name] || bound == self[left_column_name]
513 return if bound == self[right_column_name] || bound == self[left_column_name]
514
514
515 # we have defined the boundaries of two non-overlapping intervals,
515 # we have defined the boundaries of two non-overlapping intervals,
516 # so sorting puts both the intervals and their boundaries in order
516 # so sorting puts both the intervals and their boundaries in order
517 a, b, c, d = [self[left_column_name], self[right_column_name], bound, other_bound].sort
517 a, b, c, d = [self[left_column_name], self[right_column_name], bound, other_bound].sort
518
518
519 new_parent = case position
519 new_parent = case position
520 when :child; target.id
520 when :child; target.id
521 when :root; nil
521 when :root; nil
522 else target[parent_column_name]
522 else target[parent_column_name]
523 end
523 end
524
524
525 self.class.base_class.update_all([
525 self.class.base_class.update_all([
526 "#{quoted_left_column_name} = CASE " +
526 "#{quoted_left_column_name} = CASE " +
527 "WHEN #{quoted_left_column_name} BETWEEN :a AND :b " +
527 "WHEN #{quoted_left_column_name} BETWEEN :a AND :b " +
528 "THEN #{quoted_left_column_name} + :d - :b " +
528 "THEN #{quoted_left_column_name} + :d - :b " +
529 "WHEN #{quoted_left_column_name} BETWEEN :c AND :d " +
529 "WHEN #{quoted_left_column_name} BETWEEN :c AND :d " +
530 "THEN #{quoted_left_column_name} + :a - :c " +
530 "THEN #{quoted_left_column_name} + :a - :c " +
531 "ELSE #{quoted_left_column_name} END, " +
531 "ELSE #{quoted_left_column_name} END, " +
532 "#{quoted_right_column_name} = CASE " +
532 "#{quoted_right_column_name} = CASE " +
533 "WHEN #{quoted_right_column_name} BETWEEN :a AND :b " +
533 "WHEN #{quoted_right_column_name} BETWEEN :a AND :b " +
534 "THEN #{quoted_right_column_name} + :d - :b " +
534 "THEN #{quoted_right_column_name} + :d - :b " +
535 "WHEN #{quoted_right_column_name} BETWEEN :c AND :d " +
535 "WHEN #{quoted_right_column_name} BETWEEN :c AND :d " +
536 "THEN #{quoted_right_column_name} + :a - :c " +
536 "THEN #{quoted_right_column_name} + :a - :c " +
537 "ELSE #{quoted_right_column_name} END, " +
537 "ELSE #{quoted_right_column_name} END, " +
538 "#{quoted_parent_column_name} = CASE " +
538 "#{quoted_parent_column_name} = CASE " +
539 "WHEN #{self.class.base_class.primary_key} = :id THEN :new_parent " +
539 "WHEN #{self.class.base_class.primary_key} = :id THEN :new_parent " +
540 "ELSE #{quoted_parent_column_name} END",
540 "ELSE #{quoted_parent_column_name} END",
541 {:a => a, :b => b, :c => c, :d => d, :id => self.id, :new_parent => new_parent}
541 {:a => a, :b => b, :c => c, :d => d, :id => self.id, :new_parent => new_parent}
542 ], nested_set_scope.proxy_options[:conditions])
542 ], nested_set_scope.proxy_options[:conditions])
543 end
543 end
544 target.reload_nested_set if target
544 target.reload_nested_set if target
545 self.reload_nested_set
545 self.reload_nested_set
546 callback(:after_move)
546 callback(:after_move)
547 end
547 end
548
548
549 end
549 end
550
550
551 end
551 end
552 end
552 end
553 end
553 end
General Comments 0
You need to be logged in to leave comments. Login now