##// END OF EJS Templates
Cleaner way to handle the replacement of watch links (#8071)....
Jean-Philippe Lang -
r5200:e3dae9ddbf46
parent child
Show More
@@ -1,101 +1,93
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class WatchersController < ApplicationController
19 19 before_filter :find_project
20 20 before_filter :require_login, :check_project_privacy, :only => [:watch, :unwatch]
21 21 before_filter :authorize, :only => [:new, :destroy]
22 22
23 23 verify :method => :post,
24 24 :only => [ :watch, :unwatch ],
25 25 :render => { :nothing => true, :status => :method_not_allowed }
26 26
27 27 def watch
28 28 if @watched.respond_to?(:visible?) && !@watched.visible?(User.current)
29 29 render_403
30 30 else
31 31 set_watcher(User.current, true)
32 32 end
33 33 end
34 34
35 35 def unwatch
36 36 set_watcher(User.current, false)
37 37 end
38 38
39 39 def new
40 40 @watcher = Watcher.new(params[:watcher])
41 41 @watcher.watchable = @watched
42 42 @watcher.save if request.post?
43 43 respond_to do |format|
44 44 format.html { redirect_to :back }
45 45 format.js do
46 46 render :update do |page|
47 47 page.replace_html 'watchers', :partial => 'watchers/watchers', :locals => {:watched => @watched}
48 48 end
49 49 end
50 50 end
51 51 rescue ::ActionController::RedirectBackError
52 52 render :text => 'Watcher added.', :layout => true
53 53 end
54 54
55 55 def destroy
56 56 @watched.set_watcher(User.find(params[:user_id]), false) if request.post?
57 57 respond_to do |format|
58 58 format.html { redirect_to :back }
59 59 format.js do
60 60 render :update do |page|
61 61 page.replace_html 'watchers', :partial => 'watchers/watchers', :locals => {:watched => @watched}
62 62 end
63 63 end
64 64 end
65 65 end
66 66
67 67 private
68 68 def find_project
69 69 klass = Object.const_get(params[:object_type].camelcase)
70 70 return false unless klass.respond_to?('watched_by')
71 71 @watched = klass.find(params[:object_id])
72 72 @project = @watched.project
73 73 rescue
74 74 render_404
75 75 end
76 76
77 77 def set_watcher(user, watching)
78 78 @watched.set_watcher(user, watching)
79 if params[:replace].present?
80 if params[:replace].is_a? Array
81 replace_ids = params[:replace]
82 else
83 replace_ids = [params[:replace]]
84 end
85 else
86 replace_ids = ['watcher']
87 end
88 79 respond_to do |format|
89 80 format.html { redirect_to :back }
90 81 format.js do
91 82 render(:update) do |page|
92 replace_ids.each do |replace_id|
93 page.replace_html replace_id, watcher_link(@watched, user, :replace => replace_ids)
83 c = watcher_css(@watched)
84 page.select(".#{c}").each do |item|
85 page.replace_html item, watcher_link(@watched, user)
94 86 end
95 87 end
96 88 end
97 89 end
98 90 rescue ::ActionController::RedirectBackError
99 91 render :text => (watching ? 'Watcher added.' : 'Watcher removed.'), :layout => true
100 92 end
101 93 end
@@ -1,69 +1,64
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 module WatchersHelper
19 19
20 # Valid options
21 # * :id - the element id
22 # * :replace - a string or array of element ids that will be
23 # replaced
24 def watcher_tag(object, user, options={:replace => 'watcher'})
25 id = options[:id]
26 id ||= options[:replace] if options[:replace].is_a? String
27 content_tag("span", watcher_link(object, user, options), :id => id)
20 def watcher_tag(object, user, options={})
21 content_tag("span", watcher_link(object, user), :class => watcher_css(object))
28 22 end
29 23
30 # Valid options
31 # * :replace - a string or array of element ids that will be
32 # replaced
33 def watcher_link(object, user, options={:replace => 'watcher'})
24 def watcher_link(object, user)
34 25 return '' unless user && user.logged? && object.respond_to?('watched_by?')
35 26 watched = object.watched_by?(user)
36 27 url = {:controller => 'watchers',
37 28 :action => (watched ? 'unwatch' : 'watch'),
38 29 :object_type => object.class.to_s.underscore,
39 :object_id => object.id,
40 :replace => options[:replace]}
30 :object_id => object.id}
41 31 link_to_remote((watched ? l(:button_unwatch) : l(:button_watch)),
42 32 {:url => url},
43 33 :href => url_for(url),
44 34 :class => (watched ? 'icon icon-fav' : 'icon icon-fav-off'))
45 35
46 36 end
47 37
38 # Returns the css class used to identify watch links for a given +object+
39 def watcher_css(object)
40 "#{object.class.to_s.underscore}-#{object.id}-watcher"
41 end
42
48 43 # Returns a comma separated list of users watching the given object
49 44 def watchers_list(object)
50 45 remove_allowed = User.current.allowed_to?("delete_#{object.class.name.underscore}_watchers".to_sym, object.project)
51 46 lis = object.watcher_users.collect do |user|
52 47 s = avatar(user, :size => "16").to_s + link_to_user(user, :class => 'user').to_s
53 48 if remove_allowed
54 49 url = {:controller => 'watchers',
55 50 :action => 'destroy',
56 51 :object_type => object.class.to_s.underscore,
57 52 :object_id => object.id,
58 53 :user_id => user}
59 54 s += ' ' + link_to_remote(image_tag('delete.png'),
60 55 {:url => url},
61 56 :href => url_for(url),
62 57 :style => "vertical-align: middle",
63 58 :class => "delete")
64 59 end
65 60 "<li>#{ s }</li>"
66 61 end
67 62 lis.empty? ? "" : "<ul>#{ lis.join("\n") }</ul>"
68 63 end
69 64 end
@@ -1,10 +1,9
1 1 <div class="contextual">
2 2 <%= link_to_if_authorized(l(:button_update), {:controller => 'issues', :action => 'edit', :id => @issue }, :onclick => 'showAndScrollTo("update", "notes"); return false;', :class => 'icon icon-edit', :accesskey => accesskey(:edit)) %>
3 3 <%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'new', :issue_id => @issue}, :class => 'icon icon-time-add' %>
4 <% replace_watcher ||= 'watcher' %>
5 <%= watcher_tag(@issue, User.current, {:id => replace_watcher, :replace => ['watcher','watcher2']}) %>
4 <%= watcher_tag(@issue, User.current) %>
6 5 <%= link_to_if_authorized l(:button_duplicate), {:controller => 'issues', :action => 'new', :project_id => @project, :copy_from => @issue }, :class => 'icon icon-duplicate' %>
7 6 <%= link_to_if_authorized l(:button_copy), {:controller => 'issue_moves', :action => 'new', :id => @issue, :copy_options => {:copy => 't'}}, :class => 'icon icon-copy' %>
8 7 <%= link_to_if_authorized l(:button_move), {:controller => 'issue_moves', :action => 'new', :id => @issue}, :class => 'icon icon-move' %>
9 8 <%= link_to_if_authorized l(:button_delete), {:controller => 'issues', :action => 'destroy', :id => @issue}, :confirm => (@issue.leaf? ? l(:text_are_you_sure) : l(:text_are_you_sure_with_children)), :method => :post, :class => 'icon icon-del' %>
10 9 </div>
@@ -1,137 +1,137
1 1 <%= render :partial => 'action_menu' %>
2 2
3 3 <h2><%= @issue.tracker.name %> #<%= @issue.id %></h2>
4 4
5 5 <div class="<%= @issue.css_classes %> details">
6 6 <%= avatar(@issue.author, :size => "50") %>
7 7
8 8 <div class="subject">
9 9 <%= render_issue_subject_with_tree(@issue) %>
10 10 </div>
11 11 <p class="author">
12 12 <%= authoring @issue.created_on, @issue.author %>.
13 13 <% if @issue.created_on != @issue.updated_on %>
14 14 <%= l(:label_updated_time, time_tag(@issue.updated_on)) %>.
15 15 <% end %>
16 16 </p>
17 17
18 18 <table class="attributes">
19 19 <tr>
20 20 <th class="status"><%=l(:field_status)%>:</th><td class="status"><%= @issue.status.name %></td>
21 21 <th class="start-date"><%=l(:field_start_date)%>:</th><td class="start-date"><%= format_date(@issue.start_date) %></td>
22 22 </tr>
23 23 <tr>
24 24 <th class="priority"><%=l(:field_priority)%>:</th><td class="priority"><%= @issue.priority.name %></td>
25 25 <th class="due-date"><%=l(:field_due_date)%>:</th><td class="due-date"><%= format_date(@issue.due_date) %></td>
26 26 </tr>
27 27 <tr>
28 28 <th class="assigned-to"><%=l(:field_assigned_to)%>:</th><td class="assigned-to"><%= avatar(@issue.assigned_to, :size => "14") %><%= @issue.assigned_to ? link_to_user(@issue.assigned_to) : "-" %></td>
29 29 <th class="progress"><%=l(:field_done_ratio)%>:</th><td class="progress"><%= progress_bar @issue.done_ratio, :width => '80px', :legend => "#{@issue.done_ratio}%" %></td>
30 30 </tr>
31 31 <tr>
32 32 <th class="category"><%=l(:field_category)%>:</th><td class="category"><%=h @issue.category ? @issue.category.name : "-" %></td>
33 33 <% if User.current.allowed_to?(:view_time_entries, @project) %>
34 34 <th class="spent-time"><%=l(:label_spent_time)%>:</th>
35 35 <td class="spent-time"><%= @issue.spent_hours > 0 ? (link_to l_hours(@issue.spent_hours), {:controller => 'timelog', :action => 'index', :project_id => @project, :issue_id => @issue}) : "-" %></td>
36 36 <% end %>
37 37 </tr>
38 38 <tr>
39 39 <th class="fixed-version"><%=l(:field_fixed_version)%>:</th><td class="fixed-version"><%= @issue.fixed_version ? link_to_version(@issue.fixed_version) : "-" %></td>
40 40 <% if @issue.estimated_hours %>
41 41 <th class="estimated-hours"><%=l(:field_estimated_hours)%>:</th><td class="estimated-hours"><%= l_hours(@issue.estimated_hours) %></td>
42 42 <% end %>
43 43 </tr>
44 44 <%= render_custom_fields_rows(@issue) %>
45 45 <%= call_hook(:view_issues_show_details_bottom, :issue => @issue) %>
46 46 </table>
47 47
48 48 <% if @issue.description? || @issue.attachments.any? -%>
49 49 <hr />
50 50 <% if @issue.description? %>
51 51 <div class="contextual">
52 52 <%= link_to_remote_if_authorized(l(:button_quote), { :url => {:controller => 'journals', :action => 'new', :id => @issue} }, :class => 'icon icon-comment') %>
53 53 </div>
54 54
55 55 <p><strong><%=l(:field_description)%></strong></p>
56 56 <div class="wiki">
57 57 <%= textilizable @issue, :description, :attachments => @issue.attachments %>
58 58 </div>
59 59 <% end %>
60 60 <%= link_to_attachments @issue %>
61 61 <% end -%>
62 62
63 63 <%= call_hook(:view_issues_show_description_bottom, :issue => @issue) %>
64 64
65 65 <% if !@issue.leaf? || User.current.allowed_to?(:manage_subtasks, @project) %>
66 66 <hr />
67 67 <div id="issue_tree">
68 68 <div class="contextual">
69 69 <%= link_to(l(:button_add), {:controller => 'issues', :action => 'new', :project_id => @project, :issue => {:parent_issue_id => @issue}}) if User.current.allowed_to?(:manage_subtasks, @project) %>
70 70 </div>
71 71 <p><strong><%=l(:label_subtask_plural)%></strong></p>
72 72 <%= render_descendants_tree(@issue) unless @issue.leaf? %>
73 73 </div>
74 74 <% end %>
75 75
76 76 <% if authorize_for('issue_relations', 'new') || @issue.relations.present? %>
77 77 <hr />
78 78 <div id="relations">
79 79 <%= render :partial => 'relations' %>
80 80 </div>
81 81 <% end %>
82 82
83 83 </div>
84 84
85 85 <% if @changesets.present? %>
86 86 <div id="issue-changesets">
87 87 <h3><%=l(:label_associated_revisions)%></h3>
88 88 <%= render :partial => 'changesets', :locals => { :changesets => @changesets} %>
89 89 </div>
90 90 <% end %>
91 91
92 92 <% if @journals.present? %>
93 93 <div id="history">
94 94 <h3><%=l(:label_history)%></h3>
95 95 <%= render :partial => 'history', :locals => { :issue => @issue, :journals => @journals } %>
96 96 </div>
97 97 <% end %>
98 98
99 99
100 100 <div style="clear: both;"></div>
101 <%= render :partial => 'action_menu', :locals => {:replace_watcher => 'watcher2' } %>
101 <%= render :partial => 'action_menu' %>
102 102
103 103 <div style="clear: both;"></div>
104 104 <% if authorize_for('issues', 'edit') %>
105 105 <div id="update" style="display:none;">
106 106 <h3><%= l(:button_update) %></h3>
107 107 <%= render :partial => 'edit' %>
108 108 </div>
109 109 <% end %>
110 110
111 111 <% other_formats_links do |f| %>
112 112 <%= f.link_to 'Atom', :url => {:key => User.current.rss_key} %>
113 113 <%= f.link_to 'PDF' %>
114 114 <% end %>
115 115
116 116 <% html_title "#{@issue.tracker.name} ##{@issue.id}: #{@issue.subject}" %>
117 117
118 118 <% content_for :sidebar do %>
119 119 <%= render :partial => 'issues/sidebar' %>
120 120
121 121 <% if User.current.allowed_to?(:add_issue_watchers, @project) ||
122 122 (@issue.watchers.present? && User.current.allowed_to?(:view_issue_watchers, @project)) %>
123 123 <div id="watchers">
124 124 <%= render :partial => 'watchers/watchers', :locals => {:watched => @issue} %>
125 125 </div>
126 126 <% end %>
127 127 <% end %>
128 128
129 129 <% content_for :header_tags do %>
130 130 <%= auto_discovery_link_tag(:atom, {:format => 'atom', :key => User.current.rss_key}, :title => "#{@issue.project} - #{@issue.tracker} ##{@issue.id}: #{@issue.subject}") %>
131 131 <%= stylesheet_link_tag 'scm' %>
132 132 <%= javascript_include_tag 'context_menu' %>
133 133 <%= stylesheet_link_tag 'context_menu' %>
134 134 <%= stylesheet_link_tag 'context_menu_rtl' if l(:direction) == 'rtl' %>
135 135 <% end %>
136 136 <div id="context-menu" style="display: none;"></div>
137 137 <%= javascript_tag "new ContextMenu('#{issues_context_menu_path}')" %>
@@ -1,110 +1,89
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../test_helper', __FILE__)
19 19 require 'watchers_controller'
20 20
21 21 # Re-raise errors caught by the controller.
22 22 class WatchersController; def rescue_action(e) raise e end; end
23 23
24 24 class WatchersControllerTest < ActionController::TestCase
25 25 fixtures :projects, :users, :roles, :members, :member_roles, :enabled_modules,
26 26 :issues, :trackers, :projects_trackers, :issue_statuses, :enumerations, :watchers
27 27
28 28 def setup
29 29 @controller = WatchersController.new
30 30 @request = ActionController::TestRequest.new
31 31 @response = ActionController::TestResponse.new
32 32 User.current = nil
33 33 end
34 34
35 35 def test_get_watch_should_be_invalid
36 36 @request.session[:user_id] = 3
37 37 get :watch, :object_type => 'issue', :object_id => '1'
38 38 assert_response 405
39 39 end
40 40
41 41 def test_watch
42 42 @request.session[:user_id] = 3
43 43 assert_difference('Watcher.count') do
44 44 xhr :post, :watch, :object_type => 'issue', :object_id => '1'
45 45 assert_response :success
46 assert_select_rjs :replace_html, 'watcher'
46 assert @response.body.include?('$$(".issue-1-watcher")')
47 47 end
48 48 assert Issue.find(1).watched_by?(User.find(3))
49 49 end
50 50
51 51 def test_watch_should_be_denied_without_permission
52 52 Role.find(2).remove_permission! :view_issues
53 53 @request.session[:user_id] = 3
54 54 assert_no_difference('Watcher.count') do
55 55 xhr :post, :watch, :object_type => 'issue', :object_id => '1'
56 56 assert_response 403
57 57 end
58 58 end
59 59
60 def test_watch_with_multiple_replacements
61 @request.session[:user_id] = 3
62 assert_difference('Watcher.count') do
63 xhr :post, :watch, :object_type => 'issue', :object_id => '1', :replace => ['watch_item_1','watch_item_2']
64 assert_response :success
65 assert_select_rjs :replace_html, 'watch_item_1'
66 assert_select_rjs :replace_html, 'watch_item_2'
67 end
68 end
69
70 60 def test_unwatch
71 61 @request.session[:user_id] = 3
72 62 assert_difference('Watcher.count', -1) do
73 63 xhr :post, :unwatch, :object_type => 'issue', :object_id => '2'
74 64 assert_response :success
75 assert_select_rjs :replace_html, 'watcher'
76 end
77 assert !Issue.find(1).watched_by?(User.find(3))
78 end
79
80 def test_unwatch_with_multiple_replacements
81 @request.session[:user_id] = 3
82 assert_difference('Watcher.count', -1) do
83 xhr :post, :unwatch, :object_type => 'issue', :object_id => '2', :replace => ['watch_item_1', 'watch_item_2']
84 assert_response :success
85 assert_select_rjs :replace_html, 'watch_item_1'
86 assert_select_rjs :replace_html, 'watch_item_2'
65 assert @response.body.include?('$$(".issue-2-watcher")')
87 66 end
88 67 assert !Issue.find(1).watched_by?(User.find(3))
89 68 end
90 69
91 70 def test_new_watcher
92 71 @request.session[:user_id] = 2
93 72 assert_difference('Watcher.count') do
94 73 xhr :post, :new, :object_type => 'issue', :object_id => '2', :watcher => {:user_id => '4'}
95 74 assert_response :success
96 75 assert_select_rjs :replace_html, 'watchers'
97 76 end
98 77 assert Issue.find(2).watched_by?(User.find(4))
99 78 end
100 79
101 80 def test_remove_watcher
102 81 @request.session[:user_id] = 2
103 82 assert_difference('Watcher.count', -1) do
104 83 xhr :post, :destroy, :object_type => 'issue', :object_id => '2', :user_id => '3'
105 84 assert_response :success
106 85 assert_select_rjs :replace_html, 'watchers'
107 86 end
108 87 assert !Issue.find(2).watched_by?(User.find(3))
109 88 end
110 89 end
General Comments 0
You need to be logged in to leave comments. Login now