##// END OF EJS Templates
Fixed that setting a status as closed should update issue closed_on attribute (#18280)....
Jean-Philippe Lang -
r13178:5052d48491a7
parent child
Show More
@@ -1,104 +1,122
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class IssueStatus < ActiveRecord::Base
18 class IssueStatus < ActiveRecord::Base
19 before_destroy :check_integrity
19 before_destroy :check_integrity
20 has_many :workflows, :class_name => 'WorkflowTransition', :foreign_key => "old_status_id"
20 has_many :workflows, :class_name => 'WorkflowTransition', :foreign_key => "old_status_id"
21 has_many :workflow_transitions_as_new_status, :class_name => 'WorkflowTransition', :foreign_key => "new_status_id"
21 has_many :workflow_transitions_as_new_status, :class_name => 'WorkflowTransition', :foreign_key => "new_status_id"
22 acts_as_list
22 acts_as_list
23
23
24 after_update :handle_is_closed_change
24 before_destroy :delete_workflow_rules
25 before_destroy :delete_workflow_rules
25
26
26 validates_presence_of :name
27 validates_presence_of :name
27 validates_uniqueness_of :name
28 validates_uniqueness_of :name
28 validates_length_of :name, :maximum => 30
29 validates_length_of :name, :maximum => 30
29 validates_inclusion_of :default_done_ratio, :in => 0..100, :allow_nil => true
30 validates_inclusion_of :default_done_ratio, :in => 0..100, :allow_nil => true
30 attr_protected :id
31 attr_protected :id
31
32
32 scope :sorted, lambda { order(:position) }
33 scope :sorted, lambda { order(:position) }
33 scope :named, lambda {|arg| where("LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip)}
34 scope :named, lambda {|arg| where("LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip)}
34
35
35 # Update all the +Issues+ setting their done_ratio to the value of their +IssueStatus+
36 # Update all the +Issues+ setting their done_ratio to the value of their +IssueStatus+
36 def self.update_issue_done_ratios
37 def self.update_issue_done_ratios
37 if Issue.use_status_for_done_ratio?
38 if Issue.use_status_for_done_ratio?
38 IssueStatus.where("default_done_ratio >= 0").each do |status|
39 IssueStatus.where("default_done_ratio >= 0").each do |status|
39 Issue.where({:status_id => status.id}).update_all({:done_ratio => status.default_done_ratio})
40 Issue.where({:status_id => status.id}).update_all({:done_ratio => status.default_done_ratio})
40 end
41 end
41 end
42 end
42
43
43 return Issue.use_status_for_done_ratio?
44 return Issue.use_status_for_done_ratio?
44 end
45 end
45
46
46 # Returns an array of all statuses the given role can switch to
47 # Returns an array of all statuses the given role can switch to
47 # Uses association cache when called more than one time
48 # Uses association cache when called more than one time
48 def new_statuses_allowed_to(roles, tracker, author=false, assignee=false)
49 def new_statuses_allowed_to(roles, tracker, author=false, assignee=false)
49 if roles && tracker
50 if roles && tracker
50 role_ids = roles.collect(&:id)
51 role_ids = roles.collect(&:id)
51 transitions = workflows.select do |w|
52 transitions = workflows.select do |w|
52 role_ids.include?(w.role_id) &&
53 role_ids.include?(w.role_id) &&
53 w.tracker_id == tracker.id &&
54 w.tracker_id == tracker.id &&
54 ((!w.author && !w.assignee) || (author && w.author) || (assignee && w.assignee))
55 ((!w.author && !w.assignee) || (author && w.author) || (assignee && w.assignee))
55 end
56 end
56 transitions.map(&:new_status).compact.sort
57 transitions.map(&:new_status).compact.sort
57 else
58 else
58 []
59 []
59 end
60 end
60 end
61 end
61
62
62 # Same thing as above but uses a database query
63 # Same thing as above but uses a database query
63 # More efficient than the previous method if called just once
64 # More efficient than the previous method if called just once
64 def find_new_statuses_allowed_to(roles, tracker, author=false, assignee=false)
65 def find_new_statuses_allowed_to(roles, tracker, author=false, assignee=false)
65 if roles.present? && tracker
66 if roles.present? && tracker
66 scope = IssueStatus.
67 scope = IssueStatus.
67 joins(:workflow_transitions_as_new_status).
68 joins(:workflow_transitions_as_new_status).
68 where(:workflows => {:old_status_id => id, :role_id => roles.map(&:id), :tracker_id => tracker.id})
69 where(:workflows => {:old_status_id => id, :role_id => roles.map(&:id), :tracker_id => tracker.id})
69
70
70 unless author && assignee
71 unless author && assignee
71 if author || assignee
72 if author || assignee
72 scope = scope.where("author = ? OR assignee = ?", author, assignee)
73 scope = scope.where("author = ? OR assignee = ?", author, assignee)
73 else
74 else
74 scope = scope.where("author = ? AND assignee = ?", false, false)
75 scope = scope.where("author = ? AND assignee = ?", false, false)
75 end
76 end
76 end
77 end
77
78
78 scope.uniq.to_a.sort
79 scope.uniq.to_a.sort
79 else
80 else
80 []
81 []
81 end
82 end
82 end
83 end
83
84
84 def <=>(status)
85 def <=>(status)
85 position <=> status.position
86 position <=> status.position
86 end
87 end
87
88
88 def to_s; name end
89 def to_s; name end
89
90
90 private
91 private
91
92
93 # Updates issues closed_on attribute when an existing status is set as closed.
94 def handle_is_closed_change
95 if is_closed_changed? && is_closed == true
96 # First we update issues that have a journal for when the current status was set,
97 # a subselect is used to update all issues with a single query
98 subselect = "SELECT MAX(j.created_on) FROM #{Journal.table_name} j" +
99 " JOIN #{JournalDetail.table_name} d ON d.journal_id = j.id" +
100 " WHERE j.journalized_type = 'Issue' AND j.journalized_id = #{Issue.table_name}.id" +
101 " AND d.property = 'attr' AND d.prop_key = 'status_id' AND d.value = :status_id"
102 Issue.where(:status_id => id, :closed_on => nil).update_all(["closed_on = (#{subselect})", :status_id => id.to_s])
103
104 # Then we update issues that don't have a journal which means the
105 # current status was set on creation
106 Issue.where(:status_id => id, :closed_on => nil).update_all("closed_on = created_on")
107 end
108 end
109
92 def check_integrity
110 def check_integrity
93 if Issue.where(:status_id => id).any?
111 if Issue.where(:status_id => id).any?
94 raise "This status is used by some issues"
112 raise "This status is used by some issues"
95 elsif Tracker.where(:default_status_id => id).any?
113 elsif Tracker.where(:default_status_id => id).any?
96 raise "This status is used as the default status by some trackers"
114 raise "This status is used as the default status by some trackers"
97 end
115 end
98 end
116 end
99
117
100 # Deletes associated workflows
118 # Deletes associated workflows
101 def delete_workflow_rules
119 def delete_workflow_rules
102 WorkflowRule.delete_all(["old_status_id = :id OR new_status_id = :id", {:id => id}])
120 WorkflowRule.delete_all(["old_status_id = :id OR new_status_id = :id", {:id => id}])
103 end
121 end
104 end
122 end
@@ -1,99 +1,133
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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 IssueStatusTest < ActiveSupport::TestCase
20 class IssueStatusTest < ActiveSupport::TestCase
21 fixtures :issue_statuses, :issues, :roles, :trackers
21 fixtures :issue_statuses, :issues, :roles, :trackers
22
22
23 def test_create
23 def test_create
24 status = IssueStatus.new :name => "Assigned"
24 status = IssueStatus.new :name => "Assigned"
25 assert !status.save
25 assert !status.save
26 # status name uniqueness
26 # status name uniqueness
27 assert_equal 1, status.errors.count
27 assert_equal 1, status.errors.count
28
28
29 status.name = "Test Status"
29 status.name = "Test Status"
30 assert status.save
30 assert status.save
31 end
31 end
32
32
33 def test_destroy
33 def test_destroy
34 status = IssueStatus.find(3)
34 status = IssueStatus.find(3)
35 assert_difference 'IssueStatus.count', -1 do
35 assert_difference 'IssueStatus.count', -1 do
36 assert status.destroy
36 assert status.destroy
37 end
37 end
38 assert_nil WorkflowTransition.where(:old_status_id => status.id).first
38 assert_nil WorkflowTransition.where(:old_status_id => status.id).first
39 assert_nil WorkflowTransition.where(:new_status_id => status.id).first
39 assert_nil WorkflowTransition.where(:new_status_id => status.id).first
40 end
40 end
41
41
42 def test_destroy_status_in_use
42 def test_destroy_status_in_use
43 # Status assigned to an Issue
43 # Status assigned to an Issue
44 status = Issue.find(1).status
44 status = Issue.find(1).status
45 assert_raise(RuntimeError, "Can't delete status") { status.destroy }
45 assert_raise(RuntimeError, "Can't delete status") { status.destroy }
46 end
46 end
47
47
48 def test_new_statuses_allowed_to
48 def test_new_statuses_allowed_to
49 WorkflowTransition.delete_all
49 WorkflowTransition.delete_all
50
50
51 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 2, :author => false, :assignee => false)
51 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 2, :author => false, :assignee => false)
52 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 3, :author => true, :assignee => false)
52 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 3, :author => true, :assignee => false)
53 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 4, :author => false, :assignee => true)
53 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 4, :author => false, :assignee => true)
54 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 5, :author => true, :assignee => true)
54 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 5, :author => true, :assignee => true)
55 status = IssueStatus.find(1)
55 status = IssueStatus.find(1)
56 role = Role.find(1)
56 role = Role.find(1)
57 tracker = Tracker.find(1)
57 tracker = Tracker.find(1)
58
58
59 assert_equal [2], status.new_statuses_allowed_to([role], tracker, false, false).map(&:id)
59 assert_equal [2], status.new_statuses_allowed_to([role], tracker, false, false).map(&:id)
60 assert_equal [2], status.find_new_statuses_allowed_to([role], tracker, false, false).map(&:id)
60 assert_equal [2], status.find_new_statuses_allowed_to([role], tracker, false, false).map(&:id)
61
61
62 assert_equal [2, 3, 5], status.new_statuses_allowed_to([role], tracker, true, false).map(&:id)
62 assert_equal [2, 3, 5], status.new_statuses_allowed_to([role], tracker, true, false).map(&:id)
63 assert_equal [2, 3, 5], status.find_new_statuses_allowed_to([role], tracker, true, false).map(&:id)
63 assert_equal [2, 3, 5], status.find_new_statuses_allowed_to([role], tracker, true, false).map(&:id)
64
64
65 assert_equal [2, 4, 5], status.new_statuses_allowed_to([role], tracker, false, true).map(&:id)
65 assert_equal [2, 4, 5], status.new_statuses_allowed_to([role], tracker, false, true).map(&:id)
66 assert_equal [2, 4, 5], status.find_new_statuses_allowed_to([role], tracker, false, true).map(&:id)
66 assert_equal [2, 4, 5], status.find_new_statuses_allowed_to([role], tracker, false, true).map(&:id)
67
67
68 assert_equal [2, 3, 4, 5], status.new_statuses_allowed_to([role], tracker, true, true).map(&:id)
68 assert_equal [2, 3, 4, 5], status.new_statuses_allowed_to([role], tracker, true, true).map(&:id)
69 assert_equal [2, 3, 4, 5], status.find_new_statuses_allowed_to([role], tracker, true, true).map(&:id)
69 assert_equal [2, 3, 4, 5], status.find_new_statuses_allowed_to([role], tracker, true, true).map(&:id)
70 end
70 end
71
71
72 def test_update_done_ratios_with_issue_done_ratio_set_to_issue_field_should_change_nothing
72 def test_update_done_ratios_with_issue_done_ratio_set_to_issue_field_should_change_nothing
73 IssueStatus.find(1).update_attribute(:default_done_ratio, 50)
73 IssueStatus.find(1).update_attribute(:default_done_ratio, 50)
74
74
75 with_settings :issue_done_ratio => 'issue_field' do
75 with_settings :issue_done_ratio => 'issue_field' do
76 IssueStatus.update_issue_done_ratios
76 IssueStatus.update_issue_done_ratios
77 assert_equal 0, Issue.where(:done_ratio => 50).count
77 assert_equal 0, Issue.where(:done_ratio => 50).count
78 end
78 end
79 end
79 end
80
80
81 def test_update_done_ratios_with_issue_done_ratio_set_to_issue_status_should_update_issues
81 def test_update_done_ratios_with_issue_done_ratio_set_to_issue_status_should_update_issues
82 IssueStatus.find(1).update_attribute(:default_done_ratio, 50)
82 IssueStatus.find(1).update_attribute(:default_done_ratio, 50)
83 with_settings :issue_done_ratio => 'issue_status' do
83 with_settings :issue_done_ratio => 'issue_status' do
84 IssueStatus.update_issue_done_ratios
84 IssueStatus.update_issue_done_ratios
85 issues = Issue.where(:status_id => 1)
85 issues = Issue.where(:status_id => 1)
86 assert_equal [50], issues.map {|issue| issue.read_attribute(:done_ratio)}.uniq
86 assert_equal [50], issues.map {|issue| issue.read_attribute(:done_ratio)}.uniq
87 end
87 end
88 end
88 end
89
89
90 def test_sorted_scope
90 def test_sorted_scope
91 assert_equal IssueStatus.all.sort, IssueStatus.sorted.to_a
91 assert_equal IssueStatus.all.sort, IssueStatus.sorted.to_a
92 end
92 end
93
93
94 def test_named_scope
94 def test_named_scope
95 status = IssueStatus.named("resolved").first
95 status = IssueStatus.named("resolved").first
96 assert_not_nil status
96 assert_not_nil status
97 assert_equal "Resolved", status.name
97 assert_equal "Resolved", status.name
98 end
98 end
99
100 def test_setting_status_as_closed_should_set_closed_on_for_issues_without_status_journal
101 issue = Issue.generate!(:status_id => 1, :created_on => 2.days.ago)
102 assert_nil issue.closed_on
103
104 issue.status.update! :is_closed => true
105
106 issue.reload
107 assert issue.closed?
108 assert_equal issue.created_on, issue.closed_on
109 end
110
111 def test_setting_status_as_closed_should_set_closed_on_for_issues_with_status_journal
112 issue = Issue.generate!(:status_id => 1, :created_on => 2.days.ago)
113 issue.init_journal(User.find(1))
114 issue.status_id = 2
115 issue.save!
116
117 issue.status.update! :is_closed => true
118
119 issue.reload
120 assert issue.closed?
121 assert_equal issue.journals.first.created_on, issue.closed_on
122 end
123
124 def test_setting_status_as_closed_should_not_set_closed_on_for_issues_with_other_status
125 issue = Issue.generate!(:status_id => 2)
126
127 IssueStatus.find(1).update! :is_closed => true
128
129 issue.reload
130 assert !issue.closed?
131 assert_nil issue.closed_on
132 end
99 end
133 end
General Comments 0
You need to be logged in to leave comments. Login now