Project

General

Profile

« Previous | Next » 

Revision 11339

Bulk watch/unwatch issues from the context menu (#7159).

View differences:

trunk/app/controllers/watchers_controller.rb
16 16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17 17

  
18 18
class WatchersController < ApplicationController
19
  before_filter :find_project
20
  before_filter :require_login, :check_project_privacy, :only => [:watch, :unwatch]
21
  before_filter :authorize, :only => [:new, :destroy]
22
  accept_api_auth :create, :destroy
19
  before_filter :require_login, :find_watchables, :only => [:watch, :unwatch]
23 20

  
24 21
  def watch
25
    if @watched.respond_to?(:visible?) && [email protected]?(User.current)
26
      render_403
27
    else
28
      set_watcher(User.current, true)
29
    end
22
    set_watcher(@watchables, User.current, true)
30 23
  end
31 24

  
32 25
  def unwatch
33
    set_watcher(User.current, false)
26
    set_watcher(@watchables, User.current, false)
34 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 32
  def new
37 33
  end
38 34

  
......
77 73
    render :layout => false
78 74
  end
79 75

  
80
private
76
  private
77

  
81 78
  def find_project
82 79
    if params[:object_type] && params[:object_id]
83 80
      klass = Object.const_get(params[:object_type].camelcase)
......
91 88
    render_404
92 89
  end
93 90

  
94
  def set_watcher(user, watching)
95
    @watched.set_watcher(user, watching)
91
  def find_watchables
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 104
    respond_to do |format|
97 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 107
    end
100 108
  end
101 109
end
trunk/app/helpers/watchers_helper.rb
24 24
    watcher_link(object, user)
25 25
  end
26 26

  
27
  def watcher_link(object, user)
28
    return '' unless user && user.logged? && object.respond_to?('watched_by?')
29
    watched = object.watched_by?(user)
30
    css = [watcher_css(object), watched ? 'icon icon-fav' : 'icon icon-fav-off'].join(' ')
31
    url = {:controller => 'watchers',
32
           :action => (watched ? 'unwatch' : 'watch'),
33
           :object_type => object.class.to_s.underscore,
34
           :object_id => object.id}
35
    link_to((watched ? l(:button_unwatch) : l(:button_watch)), url,
36
            :remote => true, :method => 'post', :class => css)
27
  def watcher_link(objects, user)
28
    return '' unless user && user.logged?
29
    objects = Array.wrap(objects)
30

  
31
    watched = objects.any? {|object| object.watched_by?(user)}
32
    css = [watcher_css(objects), watched ? 'icon icon-fav' : 'icon icon-fav-off'].join(' ')
33
    text = watched ? l(:button_unwatch) : l(:button_watch)
34
    url = {
35
      :controller => 'watchers',
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 42
  end
38 43

  
39 44
  # Returns the css class used to identify watch links for a given +object+
40
  def watcher_css(object)
41
    "#{object.class.to_s.underscore}-#{object.id}-watcher"
45
  def watcher_css(objects)
46
    objects = Array.wrap(objects)
47
    id = (objects.size == 1 ? objects.first.id : 'bulk')
48
    "#{objects.first.class.to_s.underscore}-#{id}-watcher"
42 49
  end
43 50

  
44 51
  # Returns a comma separated list of users watching the given object
trunk/app/views/context_menus/issues.html.erb
117 117
    </li>
118 118
  <% end %>
119 119

  
120
<% if User.current.logged? %>
121
  <li><%= watcher_link(@issues, User.current) %></li>
122
<% end %>
123

  
120 124
<% if @issue.present? %>
121
  <% if User.current.logged? %>
122
  <li><%= watcher_link(@issue, User.current) %></li>
123
  <% end %>
124 125
  <% if @can[:log_time] -%>
125 126
  <li><%= context_menu_link l(:button_log_time), new_issue_time_entry_path(@issue),
126 127
          :class => 'icon-time-add' %></li>
trunk/app/views/watchers/_new.html.erb
2 2

  
3 3
<%= form_tag({:controller => 'watchers',
4 4
              :action => (watched ? 'create' : 'append'),
5
              :object_type => watched.class.name.underscore,
6
              :object_id => watched},
5
              :object_type => (watched && watched.class.name.underscore),
6
              :object_id => watched,
7
              :project_id => @project},
7 8
             :remote => true,
8 9
             :method => :post,
9 10
             :id => 'new-watcher-form') do %>
......
11 12
  <p><%= label_tag 'user_search', l(:label_user_search) %><%= text_field_tag 'user_search', nil %></p>
12 13
  <%= javascript_tag "observeSearchfield('user_search', 'users_for_watcher', '#{ escape_javascript url_for(:controller => 'watchers',
13 14
                 :action => 'autocomplete_for_user',
14
                 :object_type => watched.class.name.underscore,
15
                 :object_id => watched) }')" %>
15
                 :object_type => (watched && watched.class.name.underscore),
16
                 :object_id => watched,
17
                 :project_id => @project) }')" %>
16 18

  
17 19
  <div id="users_for_watcher">
18 20
    <%= principals_check_box_tags 'watcher[user_ids][]', (watched ? watched.addable_watcher_users : User.active.all(:limit => 100)) %>
trunk/lib/redmine.rb
127 127
    map.permission :save_queries, {:queries => [:new, :create, :edit, :update, :destroy]}, :require => :loggedin
128 128
    # Watchers
129 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 131
    map.permission :delete_issue_watchers, {:watchers => :destroy}
132 132
  end
133 133

  
trunk/test/functional/watchers_controller_test.rb
25 25
    User.current = nil
26 26
  end
27 27

  
28
  def test_watch
28
  def test_watch_a_single_object
29 29
    @request.session[:user_id] = 3
30 30
    assert_difference('Watcher.count') do
31 31
      xhr :post, :watch, :object_type => 'issue', :object_id => '1'
......
35 35
    assert Issue.find(1).watched_by?(User.find(3))
36 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 59
  def test_watch_should_be_denied_without_permission
39 60
    Role.find(2).remove_permission! :view_issues
40 61
    @request.session[:user_id] = 3
......
70 91
    assert !Issue.find(1).watched_by?(User.find(3))
71 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 108
  def test_new
74 109
    @request.session[:user_id] = 2
75 110
    xhr :get, :new, :object_type => 'issue', :object_id => '2'
......
77 112
    assert_match /ajax-modal/, response.body
78 113
  end
79 114

  
80
  def test_new_for_new_record_with_id
115
  def test_new_for_new_record_with_project_id
81 116
    @request.session[:user_id] = 2
82 117
    xhr :get, :new, :project_id => 1
83 118
    assert_response :success
......
85 120
    assert_match /ajax-modal/, response.body
86 121
  end
87 122

  
88
  def test_new_for_new_record_with_identifier
123
  def test_new_for_new_record_with_project_identifier
89 124
    @request.session[:user_id] = 2
90 125
    xhr :get, :new, :project_id => 'ecookbook'
91 126
    assert_response :success
......
117 152
  end
118 153

  
119 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 157
    assert_response :success
122 158
    assert_select 'input', :count => 4
123 159
    assert_select 'input[name=?][value=1]', 'watcher[user_ids][]'
......
127 163
  end
128 164

  
129 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 168
    assert_response :success
132 169
    assert_select 'input', :count => 3
133 170
    assert_select 'input[name=?][value=2]', 'watcher[user_ids][]'
......
139 176
  def test_append
140 177
    @request.session[:user_id] = 2
141 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 180
      assert_response :success
144 181
      assert_include 'watchers_inputs', response.body
145 182
      assert_include 'issue[watcher_user_ids][]', response.body
trunk/test/unit/helpers/watchers_helper_test.rb
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
0 70

  

Also available in: Unified diff