##// END OF EJS Templates
Allow additional workflow transitions for issue author and assignee (#2732)....
Jean-Philippe Lang -
r4775:4b096e9a5676
parent child
Show More
@@ -0,0 +1,40
1 <table class="list transitions-<%= name %>">
2 <thead>
3 <tr>
4 <th align="left">
5 <%= link_to_function(image_tag('toggle_check.png'), "toggleCheckboxesBySelector('table.transitions-#{name} input')",
6 :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}") %>
7 <%=l(:label_current_status)%>
8 </th>
9 <th align="center" colspan="<%= @statuses.length %>"><%=l(:label_new_statuses_allowed)%></th>
10 </tr>
11 <tr>
12 <td></td>
13 <% for new_status in @statuses %>
14 <td width="<%= 75 / @statuses.size %>%" align="center">
15 <%= link_to_function(image_tag('toggle_check.png'), "toggleCheckboxesBySelector('table.transitions-#{name} input.new-status-#{new_status.id}')",
16 :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}") %>
17 <%=h new_status.name %>
18 </td>
19 <% end %>
20 </tr>
21 </thead>
22 <tbody>
23 <% for old_status in @statuses %>
24 <tr class="<%= cycle("odd", "even") %>">
25 <td>
26 <%= link_to_function(image_tag('toggle_check.png'), "toggleCheckboxesBySelector('table.transitions-#{name} input.old-status-#{old_status.id}')",
27 :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}") %>
28
29 <%=h old_status.name %>
30 </td>
31 <% for new_status in @statuses -%>
32 <td align="center">
33 <%= check_box_tag "issue_status[#{ old_status.id }][#{new_status.id}][]", name, workflows.detect {|w| w.old_status_id == old_status.id && w.new_status_id == new_status.id},
34 :class => "old-status-#{old_status.id} new-status-#{new_status.id}" %>
35 </td>
36 <% end -%>
37 </tr>
38 <% end %>
39 </tbody>
40 </table> No newline at end of file
@@ -0,0 +1,13
1 class AddWorkflowsAssigneeAndAuthor < ActiveRecord::Migration
2 def self.up
3 add_column :workflows, :assignee, :boolean, :null => false, :default => false
4 add_column :workflows, :author, :boolean, :null => false, :default => false
5 Workflow.update_all("assignee = #{Workflow.connection.quoted_false}")
6 Workflow.update_all("author = #{Workflow.connection.quoted_false}")
7 end
8
9 def self.down
10 remove_column :workflows, :assignee
11 remove_column :workflows, :author
12 end
13 end
@@ -32,14 +32,17 class WorkflowsController < ApplicationController
32
32
33 if request.post?
33 if request.post?
34 Workflow.destroy_all( ["role_id=? and tracker_id=?", @role.id, @tracker.id])
34 Workflow.destroy_all( ["role_id=? and tracker_id=?", @role.id, @tracker.id])
35 (params[:issue_status] || []).each { |old, news|
35 (params[:issue_status] || []).each { |status_id, transitions|
36 news.each { |new|
36 transitions.each { |new_status_id, options|
37 @role.workflows.build(:tracker_id => @tracker.id, :old_status_id => old, :new_status_id => new)
37 author = options.is_a?(Array) && options.include?('author') && !options.include?('always')
38 assignee = options.is_a?(Array) && options.include?('assignee') && !options.include?('always')
39 @role.workflows.build(:tracker_id => @tracker.id, :old_status_id => status_id, :new_status_id => new_status_id, :author => author, :assignee => assignee)
38 }
40 }
39 }
41 }
40 if @role.save
42 if @role.save
41 flash[:notice] = l(:notice_successful_update)
43 flash[:notice] = l(:notice_successful_update)
42 redirect_to :action => 'edit', :role_id => @role, :tracker_id => @tracker
44 redirect_to :action => 'edit', :role_id => @role, :tracker_id => @tracker
45 return
43 end
46 end
44 end
47 end
45
48
@@ -48,6 +51,14 class WorkflowsController < ApplicationController
48 @statuses = @tracker.issue_statuses
51 @statuses = @tracker.issue_statuses
49 end
52 end
50 @statuses ||= IssueStatus.find(:all, :order => 'position')
53 @statuses ||= IssueStatus.find(:all, :order => 'position')
54
55 if @tracker && @role && @statuses.any?
56 workflows = Workflow.all(:conditions => {:role_id => @role.id, :tracker_id => @tracker.id})
57 @workflows = {}
58 @workflows['always'] = workflows.select {|w| !w.author && !w.assignee}
59 @workflows['author'] = workflows.select {|w| w.author}
60 @workflows['assignee'] = workflows.select {|w| w.assignee}
61 end
51 end
62 end
52
63
53 def copy
64 def copy
@@ -422,7 +422,12 class Issue < ActiveRecord::Base
422
422
423 # Returns an array of status that user is able to apply
423 # Returns an array of status that user is able to apply
424 def new_statuses_allowed_to(user, include_default=false)
424 def new_statuses_allowed_to(user, include_default=false)
425 statuses = status.find_new_statuses_allowed_to(user.roles_for_project(project), tracker)
425 statuses = status.find_new_statuses_allowed_to(
426 user.roles_for_project(project),
427 tracker,
428 author == user,
429 assigned_to_id_changed? ? assigned_to_id_was == user.id : assigned_to_id == user.id
430 )
426 statuses << status unless statuses.empty?
431 statuses << status unless statuses.empty?
427 statuses << IssueStatus.default if include_default
432 statuses << IssueStatus.default if include_default
428 statuses = statuses.uniq.sort
433 statuses = statuses.uniq.sort
@@ -50,10 +50,16 class IssueStatus < ActiveRecord::Base
50
50
51 # Returns an array of all statuses the given role can switch to
51 # Returns an array of all statuses the given role can switch to
52 # Uses association cache when called more than one time
52 # Uses association cache when called more than one time
53 def new_statuses_allowed_to(roles, tracker)
53 def new_statuses_allowed_to(roles, tracker, author=false, assignee=false)
54 if roles && tracker
54 if roles && tracker
55 role_ids = roles.collect(&:id)
55 role_ids = roles.collect(&:id)
56 new_statuses = workflows.select {|w| role_ids.include?(w.role_id) && w.tracker_id == tracker.id}.collect{|w| w.new_status}.compact.sort
56 transitions = workflows.select do |w|
57 role_ids.include?(w.role_id) &&
58 w.tracker_id == tracker.id &&
59 (author || !w.author) &&
60 (assignee || !w.assignee)
61 end
62 transitions.collect{|w| w.new_status}.compact.sort
57 else
63 else
58 []
64 []
59 end
65 end
@@ -61,24 +67,19 class IssueStatus < ActiveRecord::Base
61
67
62 # Same thing as above but uses a database query
68 # Same thing as above but uses a database query
63 # More efficient than the previous method if called just once
69 # More efficient than the previous method if called just once
64 def find_new_statuses_allowed_to(roles, tracker)
70 def find_new_statuses_allowed_to(roles, tracker, author=false, assignee=false)
65 if roles && tracker
71 if roles && tracker
72 conditions = {:role_id => roles.collect(&:id), :tracker_id => tracker.id}
73 conditions[:author] = false unless author
74 conditions[:assignee] = false unless assignee
75
66 workflows.find(:all,
76 workflows.find(:all,
67 :include => :new_status,
77 :include => :new_status,
68 :conditions => { :role_id => roles.collect(&:id),
78 :conditions => conditions).collect{|w| w.new_status}.compact.sort
69 :tracker_id => tracker.id}).collect{ |w| w.new_status }.compact.sort
70 else
79 else
71 []
80 []
72 end
81 end
73 end
82 end
74
75 def new_status_allowed_to?(status, roles, tracker)
76 if status && roles && tracker
77 !workflows.find(:first, :conditions => {:new_status_id => status.id, :role_id => roles.collect(&:id), :tracker_id => tracker.id}).nil?
78 else
79 false
80 end
81 end
82
83
83 def <=>(status)
84 def <=>(status)
84 position <=> status.position
85 position <=> status.position
@@ -20,54 +20,31
20 </p>
20 </p>
21 <% end %>
21 <% end %>
22
22
23
24 <% if @tracker && @role && @statuses.any? %>
23 <% if @tracker && @role && @statuses.any? %>
25 <% form_tag({}, :id => 'workflow_form' ) do %>
24 <% form_tag({}, :id => 'workflow_form' ) do %>
26 <%= hidden_field_tag 'tracker_id', @tracker.id %>
25 <%= hidden_field_tag 'tracker_id', @tracker.id %>
27 <%= hidden_field_tag 'role_id', @role.id %>
26 <%= hidden_field_tag 'role_id', @role.id %>
28 <div class="autoscroll">
27 <div class="autoscroll">
29 <table class="list">
28 <%= render :partial => 'form', :locals => {:name => 'always', :workflows => @workflows['always']} %>
30 <thead>
29
31 <tr>
30 <fieldset class="collapsible" style="padding: 0; margin-top: 0.5em;">
32 <th align="left"><%=l(:label_current_status)%></th>
31 <legend onclick="toggleFieldset(this);">Autorisations supplémentaires lorsque l'utilisateur a créé la demande</legend>
33 <th align="center" colspan="<%= @statuses.length %>"><%=l(:label_new_statuses_allowed)%></th>
32 <div id="author_workflows" style="margin: 0.5em 0 0.5em 0;">
34 </tr>
33 <%= render :partial => 'form', :locals => {:name => 'author', :workflows => @workflows['author']} %>
35 <tr>
34 </div>
36 <td></td>
35 </fieldset>
37 <% for new_status in @statuses %>
36 <%= javascript_tag "hideFieldset($('author_workflows'))" unless @workflows['author'].present? %>
38 <td width="<%= 75 / @statuses.size %>%" align="center">
37
39 <%= link_to_function(image_tag('toggle_check.png'), "toggleCheckboxesBySelector('input.new-status-#{new_status.id}')",
38 <fieldset class="collapsible" style="padding: 0;">
40 :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}") %>
39 <legend onclick="toggleFieldset(this);">Autorisations supplΓ©mentaires lorsque la demande est assignΓ©e Γ  l'utilisateur</legend>
41 <%= new_status.name %>
40 <div id="assignee_workflows" style="margin: 0.5em 0 0.5em 0;">
42 </td>
41 <%= render :partial => 'form', :locals => {:name => 'assignee', :workflows => @workflows['assignee']} %>
43 <% end %>
42 </div>
44 </tr>
43 </fieldset>
45 </thead>
44 <%= javascript_tag "hideFieldset($('assignee_workflows'))" unless @workflows['assignee'].present? %>
46 <tbody>
45 </div>
47 <% for old_status in @statuses %>
46 <%= submit_tag l(:button_save) %>
48 <tr class="<%= cycle("odd", "even") %>">
47 <% end %>
49 <td>
50 <%= link_to_function(image_tag('toggle_check.png'), "toggleCheckboxesBySelector('input.old-status-#{old_status.id}')",
51 :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}") %>
52
53 <%= old_status.name %>
54 </td>
55 <% new_status_ids_allowed = old_status.find_new_statuses_allowed_to([@role], @tracker).collect(&:id) -%>
56 <% for new_status in @statuses -%>
57 <td align="center">
58 <%= check_box_tag "issue_status[#{ old_status.id }][]", new_status.id, new_status_ids_allowed.include?(new_status.id),
59 :class => "old-status-#{old_status.id} new-status-#{new_status.id}" %>
60 </td>
61 <% end -%>
62 </tr>
63 <% end %>
64 </tbody>
65 </table>
66 </div>
67 <p><%= check_all_links 'workflow_form' %></p>
68
69 <%= submit_tag l(:button_save) %>
70 <% end %>
71 <% end %>
48 <% end %>
72
49
73 <% html_title(l(:label_workflow)) -%>
50 <% html_title(l(:label_workflow)) -%>
@@ -46,6 +46,12 function toggleFieldset(el) {
46 Effect.toggle(fieldset.down('div'), 'slide', {duration:0.2});
46 Effect.toggle(fieldset.down('div'), 'slide', {duration:0.2});
47 }
47 }
48
48
49 function hideFieldset(el) {
50 var fieldset = Element.up(el, 'fieldset');
51 fieldset.toggleClassName('collapsed');
52 fieldset.down('div').hide();
53 }
54
49 var fileFieldCount = 1;
55 var fileFieldCount = 1;
50
56
51 function addFileField() {
57 function addFileField() {
@@ -1,31 +1,37
1 ---
1 ---
2 issue_statuses_006:
3 name: Rejected
4 is_default: false
5 is_closed: true
6 id: 6
7 issue_statuses_001:
2 issue_statuses_001:
3 id: 1
8 name: New
4 name: New
9 is_default: true
5 is_default: true
10 is_closed: false
6 is_closed: false
11 id: 1
7 position: 1
12 issue_statuses_002:
8 issue_statuses_002:
9 id: 2
13 name: Assigned
10 name: Assigned
14 is_default: false
11 is_default: false
15 is_closed: false
12 is_closed: false
16 id: 2
13 position: 2
17 issue_statuses_003:
14 issue_statuses_003:
15 id: 3
18 name: Resolved
16 name: Resolved
19 is_default: false
17 is_default: false
20 is_closed: false
18 is_closed: false
21 id: 3
19 position: 3
22 issue_statuses_004:
20 issue_statuses_004:
23 name: Feedback
21 name: Feedback
22 id: 4
24 is_default: false
23 is_default: false
25 is_closed: false
24 is_closed: false
26 id: 4
25 position: 4
27 issue_statuses_005:
26 issue_statuses_005:
27 id: 5
28 name: Closed
28 name: Closed
29 is_default: false
29 is_default: false
30 is_closed: true
30 is_closed: true
31 id: 5
31 position: 5
32 issue_statuses_006:
33 id: 6
34 name: Rejected
35 is_default: false
36 is_closed: true
37 position: 6
@@ -65,17 +65,17 class WorkflowsControllerTest < ActionController::TestCase
65
65
66 # allowed transitions
66 # allowed transitions
67 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
67 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
68 :name => 'issue_status[3][]',
68 :name => 'issue_status[3][5][]',
69 :value => '5',
69 :value => 'always',
70 :checked => 'checked' }
70 :checked => 'checked' }
71 # not allowed
71 # not allowed
72 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
72 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
73 :name => 'issue_status[3][]',
73 :name => 'issue_status[3][2][]',
74 :value => '2',
74 :value => 'always',
75 :checked => nil }
75 :checked => nil }
76 # unused
76 # unused
77 assert_no_tag :tag => 'input', :attributes => { :type => 'checkbox',
77 assert_no_tag :tag => 'input', :attributes => { :type => 'checkbox',
78 :name => 'issue_status[4][]' }
78 :name => 'issue_status[1][1][]' }
79 end
79 end
80
80
81 def test_get_edit_with_role_and_tracker_and_all_statuses
81 def test_get_edit_with_role_and_tracker_and_all_statuses
@@ -89,13 +89,17 class WorkflowsControllerTest < ActionController::TestCase
89 assert_equal IssueStatus.count, assigns(:statuses).size
89 assert_equal IssueStatus.count, assigns(:statuses).size
90
90
91 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
91 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
92 :name => 'issue_status[1][]',
92 :name => 'issue_status[1][1][]',
93 :value => '1',
93 :value => 'always',
94 :checked => nil }
94 :checked => nil }
95 end
95 end
96
96
97 def test_post_edit
97 def test_post_edit
98 post :edit, :role_id => 2, :tracker_id => 1, :issue_status => {'4' => ['5'], '3' => ['1', '2']}
98 post :edit, :role_id => 2, :tracker_id => 1,
99 :issue_status => {
100 '4' => {'5' => ['always']},
101 '3' => {'1' => ['always'], '2' => ['always']}
102 }
99 assert_redirected_to '/workflows/edit?role_id=2&tracker_id=1'
103 assert_redirected_to '/workflows/edit?role_id=2&tracker_id=1'
100
104
101 assert_equal 3, Workflow.count(:conditions => {:tracker_id => 1, :role_id => 2})
105 assert_equal 3, Workflow.count(:conditions => {:tracker_id => 1, :role_id => 2})
@@ -103,6 +107,30 class WorkflowsControllerTest < ActionController::TestCase
103 assert_nil Workflow.find(:first, :conditions => {:role_id => 2, :tracker_id => 1, :old_status_id => 5, :new_status_id => 4})
107 assert_nil Workflow.find(:first, :conditions => {:role_id => 2, :tracker_id => 1, :old_status_id => 5, :new_status_id => 4})
104 end
108 end
105
109
110 def test_post_edit_with_additional_transitions
111 post :edit, :role_id => 2, :tracker_id => 1,
112 :issue_status => {
113 '4' => {'5' => ['always']},
114 '3' => {'1' => ['author'], '2' => ['assignee'], '4' => ['author', 'assignee']}
115 }
116 assert_redirected_to '/workflows/edit?role_id=2&tracker_id=1'
117
118 assert_equal 4, Workflow.count(:conditions => {:tracker_id => 1, :role_id => 2})
119
120 w = Workflow.find(:first, :conditions => {:role_id => 2, :tracker_id => 1, :old_status_id => 4, :new_status_id => 5})
121 assert ! w.author
122 assert ! w.assignee
123 w = Workflow.find(:first, :conditions => {:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 1})
124 assert w.author
125 assert ! w.assignee
126 w = Workflow.find(:first, :conditions => {:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 2})
127 assert ! w.author
128 assert w.assignee
129 w = Workflow.find(:first, :conditions => {:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 4})
130 assert w.author
131 assert w.assignee
132 end
133
106 def test_clear_workflow
134 def test_clear_workflow
107 assert Workflow.count(:conditions => {:tracker_id => 1, :role_id => 2}) > 0
135 assert Workflow.count(:conditions => {:tracker_id => 1, :role_id => 2}) > 0
108
136
@@ -18,7 +18,7
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
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"
@@ -68,6 +68,30 class IssueStatusTest < ActiveSupport::TestCase
68 status.reload
68 status.reload
69 assert status.is_default?
69 assert status.is_default?
70 end
70 end
71
72 def test_new_statuses_allowed_to
73 Workflow.delete_all
74
75 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 2, :author => false, :assignee => false)
76 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 3, :author => true, :assignee => false)
77 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 4, :author => false, :assignee => true)
78 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 5, :author => true, :assignee => true)
79 status = IssueStatus.find(1)
80 role = Role.find(1)
81 tracker = Tracker.find(1)
82
83 assert_equal [2], status.new_statuses_allowed_to([role], tracker, false, false).map(&:id)
84 assert_equal [2], status.find_new_statuses_allowed_to([role], tracker, false, false).map(&:id)
85
86 assert_equal [2, 3], status.new_statuses_allowed_to([role], tracker, true, false).map(&:id)
87 assert_equal [2, 3], status.find_new_statuses_allowed_to([role], tracker, true, false).map(&:id)
88
89 assert_equal [2, 4], status.new_statuses_allowed_to([role], tracker, false, true).map(&:id)
90 assert_equal [2, 4], status.find_new_statuses_allowed_to([role], tracker, false, true).map(&:id)
91
92 assert_equal [2, 3, 4, 5], status.new_statuses_allowed_to([role], tracker, true, true).map(&:id)
93 assert_equal [2, 3, 4, 5], status.find_new_statuses_allowed_to([role], tracker, true, true).map(&:id)
94 end
71
95
72 context "#update_done_ratios" do
96 context "#update_done_ratios" do
73 setup do
97 setup do
@@ -210,6 +210,33 class IssueTest < ActiveSupport::TestCase
210 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
210 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
211 end
211 end
212
212
213
214
215 def test_new_statuses_allowed_to
216 Workflow.delete_all
217
218 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 2, :author => false, :assignee => false)
219 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 3, :author => true, :assignee => false)
220 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 4, :author => false, :assignee => true)
221 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 5, :author => true, :assignee => true)
222 status = IssueStatus.find(1)
223 role = Role.find(1)
224 tracker = Tracker.find(1)
225 user = User.find(2)
226
227 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1)
228 assert_equal [1, 2], issue.new_statuses_allowed_to(user).map(&:id)
229
230 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1, :author => user)
231 assert_equal [1, 2, 3], issue.new_statuses_allowed_to(user).map(&:id)
232
233 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1, :assigned_to => user)
234 assert_equal [1, 2, 4], issue.new_statuses_allowed_to(user).map(&:id)
235
236 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1, :author => user, :assigned_to => user)
237 assert_equal [1, 2, 3, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
238 end
239
213 def test_copy
240 def test_copy
214 issue = Issue.new.copy_from(1)
241 issue = Issue.new.copy_from(1)
215 assert issue.save
242 assert issue.save
General Comments 0
You need to be logged in to leave comments. Login now