##// END OF EJS Templates
Bulk watch/unwatch issues from the context menu (#7159)....
Jean-Philippe Lang -
r11109:856ef810b485
parent child
Show More
@@ -0,0 +1,69
1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
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
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 require File.expand_path('../../../test_helper', __FILE__)
19
20 class WatchersHelperTest < ActionView::TestCase
21 include WatchersHelper
22 include Redmine::I18n
23
24 fixtures :users, :issues
25
26 def setup
27 super
28 set_language_if_valid('en')
29 User.current = nil
30 end
31
32 test '#watcher_link with a non-watched object' do
33 expected = link_to(
34 "Watch",
35 "/watchers/watch?object_id=1&object_type=issue",
36 :remote => true, :method => 'post', :class => "issue-1-watcher icon icon-fav-off"
37 )
38 assert_equal expected, watcher_link(Issue.find(1), User.find(1))
39 end
40
41 test '#watcher_link with a single objet array' do
42 expected = link_to(
43 "Watch",
44 "/watchers/watch?object_id=1&object_type=issue",
45 :remote => true, :method => 'post', :class => "issue-1-watcher icon icon-fav-off"
46 )
47 assert_equal expected, watcher_link([Issue.find(1)], User.find(1))
48 end
49
50 test '#watcher_link with a multiple objets array' do
51 expected = link_to(
52 "Watch",
53 "/watchers/watch?object_id%5B%5D=1&object_id%5B%5D=3&object_type=issue",
54 :remote => true, :method => 'post', :class => "issue-bulk-watcher icon icon-fav-off"
55 )
56 assert_equal expected, watcher_link([Issue.find(1), Issue.find(3)], User.find(1))
57 end
58
59 test '#watcher_link with a watched object' do
60 Watcher.create!(:watchable => Issue.find(1), :user => User.find(1))
61
62 expected = link_to(
63 "Unwatch",
64 "/watchers/unwatch?object_id=1&object_type=issue",
65 :remote => true, :method => 'post', :class => "issue-1-watcher icon icon-fav"
66 )
67 assert_equal expected, watcher_link(Issue.find(1), User.find(1))
68 end
69 end
@@ -16,23 +16,19
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 WatchersController < ApplicationController
18 class WatchersController < ApplicationController
19 before_filter :find_project
19 before_filter :require_login, :find_watchables, :only => [:watch, :unwatch]
20 before_filter :require_login, :check_project_privacy, :only => [:watch, :unwatch]
21 before_filter :authorize, :only => [:new, :destroy]
22 accept_api_auth :create, :destroy
23
20
24 def watch
21 def watch
25 if @watched.respond_to?(:visible?) && !@watched.visible?(User.current)
22 set_watcher(@watchables, User.current, true)
26 render_403
27 else
28 set_watcher(User.current, true)
29 end
30 end
23 end
31
24
32 def unwatch
25 def unwatch
33 set_watcher(User.current, false)
26 set_watcher(@watchables, User.current, false)
34 end
27 end
35
28
29 before_filter :find_project, :authorize, :only => [:new, :create, :append, :destroy, :autocomplete_for_user]
30 accept_api_auth :create, :destroy
31
36 def new
32 def new
37 end
33 end
38
34
@@ -77,7 +73,8 class WatchersController < ApplicationController
77 render :layout => false
73 render :layout => false
78 end
74 end
79
75
80 private
76 private
77
81 def find_project
78 def find_project
82 if params[:object_type] && params[:object_id]
79 if params[:object_type] && params[:object_id]
83 klass = Object.const_get(params[:object_type].camelcase)
80 klass = Object.const_get(params[:object_type].camelcase)
@@ -91,11 +88,22 private
91 render_404
88 render_404
92 end
89 end
93
90
94 def set_watcher(user, watching)
91 def find_watchables
95 @watched.set_watcher(user, watching)
92 klass = Object.const_get(params[:object_type].camelcase) rescue nil
93 if klass && klass.respond_to?('watched_by')
94 @watchables = klass.find_all_by_id(Array.wrap(params[:object_id]))
95 raise Unauthorized if @watchables.any? {|w| w.respond_to?(:visible?) && !w.visible?}
96 end
97 render_404 unless @watchables.present?
98 end
99
100 def set_watcher(watchables, user, watching)
101 watchables.each do |watchable|
102 watchable.set_watcher(user, watching)
103 end
96 respond_to do |format|
104 respond_to do |format|
97 format.html { redirect_to_referer_or {render :text => (watching ? 'Watcher added.' : 'Watcher removed.'), :layout => true}}
105 format.html { redirect_to_referer_or {render :text => (watching ? 'Watcher added.' : 'Watcher removed.'), :layout => true}}
98 format.js { render :partial => 'set_watcher', :locals => {:user => user, :watched => @watched} }
106 format.js { render :partial => 'set_watcher', :locals => {:user => user, :watched => watchables} }
99 end
107 end
100 end
108 end
101 end
109 end
@@ -24,21 +24,28 module WatchersHelper
24 watcher_link(object, user)
24 watcher_link(object, user)
25 end
25 end
26
26
27 def watcher_link(object, user)
27 def watcher_link(objects, user)
28 return '' unless user && user.logged? && object.respond_to?('watched_by?')
28 return '' unless user && user.logged?
29 watched = object.watched_by?(user)
29 objects = Array.wrap(objects)
30 css = [watcher_css(object), watched ? 'icon icon-fav' : 'icon icon-fav-off'].join(' ')
30
31 url = {:controller => 'watchers',
31 watched = objects.any? {|object| object.watched_by?(user)}
32 :action => (watched ? 'unwatch' : 'watch'),
32 css = [watcher_css(objects), watched ? 'icon icon-fav' : 'icon icon-fav-off'].join(' ')
33 :object_type => object.class.to_s.underscore,
33 text = watched ? l(:button_unwatch) : l(:button_watch)
34 :object_id => object.id}
34 url = {
35 link_to((watched ? l(:button_unwatch) : l(:button_watch)), url,
35 :controller => 'watchers',
36 :remote => true, :method => 'post', :class => css)
36 :action => (watched ? 'unwatch' : 'watch'),
37 :object_type => objects.first.class.to_s.underscore,
38 :object_id => (objects.size == 1 ? objects.first.id : objects.map(&:id).sort)
39 }
40
41 link_to text, url, :remote => true, :method => 'post', :class => css
37 end
42 end
38
43
39 # Returns the css class used to identify watch links for a given +object+
44 # Returns the css class used to identify watch links for a given +object+
40 def watcher_css(object)
45 def watcher_css(objects)
41 "#{object.class.to_s.underscore}-#{object.id}-watcher"
46 objects = Array.wrap(objects)
47 id = (objects.size == 1 ? objects.first.id : 'bulk')
48 "#{objects.first.class.to_s.underscore}-#{id}-watcher"
42 end
49 end
43
50
44 # Returns a comma separated list of users watching the given object
51 # Returns a comma separated list of users watching the given object
@@ -117,10 +117,11
117 </li>
117 </li>
118 <% end %>
118 <% end %>
119
119
120 <% if User.current.logged? %>
121 <li><%= watcher_link(@issues, User.current) %></li>
122 <% end %>
123
120 <% if @issue.present? %>
124 <% if @issue.present? %>
121 <% if User.current.logged? %>
122 <li><%= watcher_link(@issue, User.current) %></li>
123 <% end %>
124 <% if @can[:log_time] -%>
125 <% if @can[:log_time] -%>
125 <li><%= context_menu_link l(:button_log_time), new_issue_time_entry_path(@issue),
126 <li><%= context_menu_link l(:button_log_time), new_issue_time_entry_path(@issue),
126 :class => 'icon-time-add' %></li>
127 :class => 'icon-time-add' %></li>
@@ -2,8 +2,9
2
2
3 <%= form_tag({:controller => 'watchers',
3 <%= form_tag({:controller => 'watchers',
4 :action => (watched ? 'create' : 'append'),
4 :action => (watched ? 'create' : 'append'),
5 :object_type => watched.class.name.underscore,
5 :object_type => (watched && watched.class.name.underscore),
6 :object_id => watched},
6 :object_id => watched,
7 :project_id => @project},
7 :remote => true,
8 :remote => true,
8 :method => :post,
9 :method => :post,
9 :id => 'new-watcher-form') do %>
10 :id => 'new-watcher-form') do %>
@@ -11,8 +12,9
11 <p><%= label_tag 'user_search', l(:label_user_search) %><%= text_field_tag 'user_search', nil %></p>
12 <p><%= label_tag 'user_search', l(:label_user_search) %><%= text_field_tag 'user_search', nil %></p>
12 <%= javascript_tag "observeSearchfield('user_search', 'users_for_watcher', '#{ escape_javascript url_for(:controller => 'watchers',
13 <%= javascript_tag "observeSearchfield('user_search', 'users_for_watcher', '#{ escape_javascript url_for(:controller => 'watchers',
13 :action => 'autocomplete_for_user',
14 :action => 'autocomplete_for_user',
14 :object_type => watched.class.name.underscore,
15 :object_type => (watched && watched.class.name.underscore),
15 :object_id => watched) }')" %>
16 :object_id => watched,
17 :project_id => @project) }')" %>
16
18
17 <div id="users_for_watcher">
19 <div id="users_for_watcher">
18 <%= principals_check_box_tags 'watcher[user_ids][]', (watched ? watched.addable_watcher_users : User.active.all(:limit => 100)) %>
20 <%= principals_check_box_tags 'watcher[user_ids][]', (watched ? watched.addable_watcher_users : User.active.all(:limit => 100)) %>
@@ -127,7 +127,7 Redmine::AccessControl.map do |map|
127 map.permission :save_queries, {:queries => [:new, :create, :edit, :update, :destroy]}, :require => :loggedin
127 map.permission :save_queries, {:queries => [:new, :create, :edit, :update, :destroy]}, :require => :loggedin
128 # Watchers
128 # Watchers
129 map.permission :view_issue_watchers, {}, :read => true
129 map.permission :view_issue_watchers, {}, :read => true
130 map.permission :add_issue_watchers, {:watchers => :new}
130 map.permission :add_issue_watchers, {:watchers => [:new, :create, :append, :autocomplete_for_user]}
131 map.permission :delete_issue_watchers, {:watchers => :destroy}
131 map.permission :delete_issue_watchers, {:watchers => :destroy}
132 end
132 end
133
133
@@ -25,7 +25,7 class WatchersControllerTest < ActionController::TestCase
25 User.current = nil
25 User.current = nil
26 end
26 end
27
27
28 def test_watch
28 def test_watch_a_single_object
29 @request.session[:user_id] = 3
29 @request.session[:user_id] = 3
30 assert_difference('Watcher.count') do
30 assert_difference('Watcher.count') do
31 xhr :post, :watch, :object_type => 'issue', :object_id => '1'
31 xhr :post, :watch, :object_type => 'issue', :object_id => '1'
@@ -35,6 +35,27 class WatchersControllerTest < ActionController::TestCase
35 assert Issue.find(1).watched_by?(User.find(3))
35 assert Issue.find(1).watched_by?(User.find(3))
36 end
36 end
37
37
38 def test_watch_a_collection_with_a_single_object
39 @request.session[:user_id] = 3
40 assert_difference('Watcher.count') do
41 xhr :post, :watch, :object_type => 'issue', :object_id => ['1']
42 assert_response :success
43 assert_include '$(".issue-1-watcher")', response.body
44 end
45 assert Issue.find(1).watched_by?(User.find(3))
46 end
47
48 def test_watch_a_collection_with_multiple_objects
49 @request.session[:user_id] = 3
50 assert_difference('Watcher.count', 2) do
51 xhr :post, :watch, :object_type => 'issue', :object_id => ['1', '3']
52 assert_response :success
53 assert_include '$(".issue-bulk-watcher")', response.body
54 end
55 assert Issue.find(1).watched_by?(User.find(3))
56 assert Issue.find(3).watched_by?(User.find(3))
57 end
58
38 def test_watch_should_be_denied_without_permission
59 def test_watch_should_be_denied_without_permission
39 Role.find(2).remove_permission! :view_issues
60 Role.find(2).remove_permission! :view_issues
40 @request.session[:user_id] = 3
61 @request.session[:user_id] = 3
@@ -70,6 +91,20 class WatchersControllerTest < ActionController::TestCase
70 assert !Issue.find(1).watched_by?(User.find(3))
91 assert !Issue.find(1).watched_by?(User.find(3))
71 end
92 end
72
93
94 def test_unwatch_a_collection_with_multiple_objects
95 @request.session[:user_id] = 3
96 Watcher.create!(:user_id => 3, :watchable => Issue.find(1))
97 Watcher.create!(:user_id => 3, :watchable => Issue.find(3))
98
99 assert_difference('Watcher.count', -2) do
100 xhr :post, :unwatch, :object_type => 'issue', :object_id => ['1', '3']
101 assert_response :success
102 assert_include '$(".issue-bulk-watcher")', response.body
103 end
104 assert !Issue.find(1).watched_by?(User.find(3))
105 assert !Issue.find(3).watched_by?(User.find(3))
106 end
107
73 def test_new
108 def test_new
74 @request.session[:user_id] = 2
109 @request.session[:user_id] = 2
75 xhr :get, :new, :object_type => 'issue', :object_id => '2'
110 xhr :get, :new, :object_type => 'issue', :object_id => '2'
@@ -77,7 +112,7 class WatchersControllerTest < ActionController::TestCase
77 assert_match /ajax-modal/, response.body
112 assert_match /ajax-modal/, response.body
78 end
113 end
79
114
80 def test_new_for_new_record_with_id
115 def test_new_for_new_record_with_project_id
81 @request.session[:user_id] = 2
116 @request.session[:user_id] = 2
82 xhr :get, :new, :project_id => 1
117 xhr :get, :new, :project_id => 1
83 assert_response :success
118 assert_response :success
@@ -85,7 +120,7 class WatchersControllerTest < ActionController::TestCase
85 assert_match /ajax-modal/, response.body
120 assert_match /ajax-modal/, response.body
86 end
121 end
87
122
88 def test_new_for_new_record_with_identifier
123 def test_new_for_new_record_with_project_identifier
89 @request.session[:user_id] = 2
124 @request.session[:user_id] = 2
90 xhr :get, :new, :project_id => 'ecookbook'
125 xhr :get, :new, :project_id => 'ecookbook'
91 assert_response :success
126 assert_response :success
@@ -117,7 +152,8 class WatchersControllerTest < ActionController::TestCase
117 end
152 end
118
153
119 def test_autocomplete_on_watchable_creation
154 def test_autocomplete_on_watchable_creation
120 xhr :get, :autocomplete_for_user, :q => 'mi'
155 @request.session[:user_id] = 2
156 xhr :get, :autocomplete_for_user, :q => 'mi', :project_id => 'ecookbook'
121 assert_response :success
157 assert_response :success
122 assert_select 'input', :count => 4
158 assert_select 'input', :count => 4
123 assert_select 'input[name=?][value=1]', 'watcher[user_ids][]'
159 assert_select 'input[name=?][value=1]', 'watcher[user_ids][]'
@@ -127,7 +163,8 class WatchersControllerTest < ActionController::TestCase
127 end
163 end
128
164
129 def test_autocomplete_on_watchable_update
165 def test_autocomplete_on_watchable_update
130 xhr :get, :autocomplete_for_user, :q => 'mi', :object_id => '2' , :object_type => 'issue'
166 @request.session[:user_id] = 2
167 xhr :get, :autocomplete_for_user, :q => 'mi', :object_id => '2' , :object_type => 'issue', :project_id => 'ecookbook'
131 assert_response :success
168 assert_response :success
132 assert_select 'input', :count => 3
169 assert_select 'input', :count => 3
133 assert_select 'input[name=?][value=2]', 'watcher[user_ids][]'
170 assert_select 'input[name=?][value=2]', 'watcher[user_ids][]'
@@ -139,7 +176,7 class WatchersControllerTest < ActionController::TestCase
139 def test_append
176 def test_append
140 @request.session[:user_id] = 2
177 @request.session[:user_id] = 2
141 assert_no_difference 'Watcher.count' do
178 assert_no_difference 'Watcher.count' do
142 xhr :post, :append, :watcher => {:user_ids => ['4', '7']}
179 xhr :post, :append, :watcher => {:user_ids => ['4', '7']}, :project_id => 'ecookbook'
143 assert_response :success
180 assert_response :success
144 assert_include 'watchers_inputs', response.body
181 assert_include 'watchers_inputs', response.body
145 assert_include 'issue[watcher_user_ids][]', response.body
182 assert_include 'issue[watcher_user_ids][]', response.body
General Comments 0
You need to be logged in to leave comments. Login now