Feature #44458 » 0001-Add-filter-form-to-project-members-settings-tab.patch
| app/controllers/members_controller.rb | ||
|---|---|---|
| 143 | 143 | |
| 144 | 144 |
def members_settings_url_params |
| 145 | 145 |
query = {:tab => 'members'}
|
| 146 |
query[:members_page] = params[:members_page] if params[:members_page].present? |
|
| 147 | 146 |
query[:per_page] = params[:per_page] if params[:per_page].present? |
| 148 |
query |
|
| 147 |
query.merge(members_list_params)
|
|
| 149 | 148 |
end |
| 150 | 149 |
end |
| app/helpers/members_helper.rb | ||
|---|---|---|
| 48 | 48 |
s + content_tag('span', links, :class => 'pagination')
|
| 49 | 49 |
end |
| 50 | 50 | |
| 51 |
# Returns the scope of the members of project matching the filters |
|
| 52 |
# set in the request params |
|
| 53 |
def members_scope(project) |
|
| 54 |
project.memberships.like(params[:member_name]).with_role(params[:member_role_id]) |
|
| 55 |
end |
|
| 56 | ||
| 51 | 57 |
# limit/offset on Member.sorted would paginate role join rows, not members |
| 52 |
def paginate_members(project)
|
|
| 53 |
ordered_ids = project.memberships.sorted.pluck("#{Member.table_name}.id").uniq
|
|
| 58 |
def paginate_members(scope)
|
|
| 59 |
ordered_ids = scope.sorted.pluck("#{Member.table_name}.id").uniq
|
|
| 54 | 60 |
member_count = ordered_ids.size |
| 55 | 61 |
member_pages = Redmine::Pagination::Paginator.new(member_count, per_page_option, params['members_page'], 'members_page') |
| 56 | 62 |
page_ids = ordered_ids[member_pages.offset, member_pages.per_page] || [] |
| 57 |
members_by_id = project.memberships.where(:id => page_ids).preload(:project, :principal, :roles).index_by(&:id)
|
|
| 63 |
members_by_id = scope.where(:id => page_ids).preload(:project, :principal, :roles).index_by(&:id)
|
|
| 58 | 64 |
members = page_ids.filter_map {|id| members_by_id[id]}
|
| 59 | 65 |
[members, member_pages, member_count] |
| 60 | 66 |
end |
| 61 | 67 | |
| 68 |
# Returns the params that define the members list currently displayed |
|
| 69 |
# (pagination and filters), so that they can be preserved across the member |
|
| 70 |
# creation, edition and deletion requests |
|
| 71 |
def members_list_params |
|
| 72 |
{
|
|
| 73 |
:members_page => params[:members_page], |
|
| 74 |
:member_name => params[:member_name], |
|
| 75 |
:member_role_id => params[:member_role_id] |
|
| 76 |
}.select {|_, value| value.present?}
|
|
| 77 |
end |
|
| 78 | ||
| 62 | 79 |
# Returns inheritance information for an inherited member role |
| 63 | 80 |
def render_role_inheritance(member, role) |
| 64 | 81 |
content = member.role_inheritance(role).filter_map do |h| |
| app/helpers/projects_helper.rb | ||
|---|---|---|
| 24 | 24 |
{:name => 'info', :action => :edit_project,
|
| 25 | 25 |
:partial => 'projects/edit', :label => :label_project}, |
| 26 | 26 |
{:name => 'members', :action => :manage_members,
|
| 27 |
:partial => 'projects/settings/members', :label => :label_member_plural}, |
|
| 27 |
:partial => 'projects/settings/members', :label => :label_member_plural, |
|
| 28 |
:url => {:tab => 'members'}.merge(members_list_params)},
|
|
| 28 | 29 |
{:name => 'issues', :action => :edit_project, :module => :issue_tracking,
|
| 29 | 30 |
:partial => 'projects/settings/issues', :label => :label_issue_tracking}, |
| 30 | 31 |
{:name => 'versions', :action => :manage_versions,
|
| app/models/member.rb | ||
|---|---|---|
| 33 | 33 |
scope :active, (lambda do |
| 34 | 34 |
joins(:principal).where(:users => {:status => Principal::STATUS_ACTIVE})
|
| 35 | 35 |
end) |
| 36 |
# Members whose principal matches the given string |
|
| 37 |
scope :like, (lambda do |arg| |
|
| 38 |
if arg.present? |
|
| 39 |
where(:user_id => Principal.like(arg).select(:id)) |
|
| 40 |
end |
|
| 41 |
end) |
|
| 42 |
# Members having the given role, inherited or not |
|
| 43 |
scope :with_role, (lambda do |arg| |
|
| 44 |
if arg.present? |
|
| 45 |
where(:id => MemberRole.where(:role_id => arg.to_i).select(:member_id)) |
|
| 46 |
end |
|
| 47 |
end) |
|
| 36 | 48 |
# Sort by first role and principal |
| 37 | 49 |
scope :sorted, (lambda do |
| 38 | 50 |
includes(:member_roles, :roles, :principal). |
| app/views/members/_edit.html.erb | ||
|---|---|---|
| 1 |
<%= form_for(@member, :url => membership_path(@member, :members_page => params[:members_page]),
|
|
| 1 |
<%= form_for(@member, :url => membership_path(@member, members_list_params),
|
|
| 2 | 2 |
:as => :membership, |
| 3 | 3 |
:remote => request.xhr?, |
| 4 | 4 |
:method => :put) do |f| %> |
| app/views/members/_new_modal.html.erb | ||
|---|---|---|
| 1 | 1 |
<h3 class="title"><%= l(:label_member_new) %></h3> |
| 2 | 2 | |
| 3 |
<%= form_for @member, :as => :membership, :url => project_memberships_path(@project, :members_page => params[:members_page]), :remote => true, :method => :post do |f| %>
|
|
| 3 |
<%= form_for @member, :as => :membership, :url => project_memberships_path(@project, members_list_params), :remote => true, :method => :post do |f| %>
|
|
| 4 | 4 |
<%= render :partial => 'new_form' %> |
| 5 | 5 |
<p class="buttons"> |
| 6 | 6 |
<%= submit_tag l(:button_add), :id => 'member-add-submit' %> |
| app/views/projects/settings/_members.html.erb | ||
|---|---|---|
| 1 |
<% members, member_pages, member_count = paginate_members(@project) %>
|
|
| 1 |
<% members, member_pages, member_count = paginate_members(members_scope(@project)) %>
|
|
| 2 | 2 | |
| 3 | 3 |
<% if User.current.admin? %> |
| 4 | 4 |
<div class="contextual"><%= link_to sprite_icon('settings', l(:label_administration)), users_path, :class => "icon icon-settings" %></div>
|
| 5 | 5 |
<% end %> |
| 6 |
<p><%= link_to sprite_icon('add', l(:label_member_new)), new_project_membership_path(@project, :members_page => params[:members_page]), :remote => true, :class => "icon icon-add" %></p>
|
|
| 6 |
<p><%= link_to sprite_icon('add', l(:label_member_new)), new_project_membership_path(@project, members_list_params), :remote => true, :class => "icon icon-add" %></p>
|
|
| 7 | ||
| 8 |
<%= form_tag(settings_project_path(@project, :tab => 'members'), :method => :get, :id => 'members-filter-form') do %> |
|
| 9 |
<fieldset><legend><%= l(:label_filter_plural) %></legend> |
|
| 10 |
<label for='member_name'><%= l(:label_user) %> / <%= l(:label_group) %>:</label> |
|
| 11 |
<%= text_field_tag 'member_name', params[:member_name], :size => 20 %> |
|
| 12 |
<label for='member_role_id'><%= l(:label_role) %>:</label> |
|
| 13 |
<%= select_tag 'member_role_id', |
|
| 14 |
options_for_select([[l(:label_all), '']] + Role.givable.pluck(:name, :id), params[:member_role_id]), |
|
| 15 |
:onchange => "this.form.submit(); return false;" %> |
|
| 16 |
<%= submit_tag l(:button_apply), :class => "small", :name => nil %> |
|
| 17 |
<%= link_to sprite_icon('reload', l(:button_clear)), settings_project_path(@project, :tab => 'members'), :class => 'icon icon-reload' %>
|
|
| 18 |
</fieldset> |
|
| 19 |
<% end %> |
|
| 20 |
|
|
| 7 | 21 | |
| 8 | 22 |
<% if member_count > 0 %> |
| 9 | 23 |
<div class="autoscroll"> |
| ... | ... | |
| 33 | 47 |
</td> |
| 34 | 48 |
<td class="buttons"> |
| 35 | 49 |
<%= link_to sprite_icon('edit', l(:button_edit)),
|
| 36 |
edit_membership_path(member, :members_page => params[:members_page]),
|
|
| 50 |
edit_membership_path(member, members_list_params),
|
|
| 37 | 51 |
:remote => true, |
| 38 | 52 |
:class => 'icon icon-edit' %> |
| 39 |
<%= remove_link membership_path(member, :members_page => params[:members_page]),
|
|
| 53 |
<%= remove_link membership_path(member, members_list_params),
|
|
| 40 | 54 |
:remote => true, |
| 41 | 55 |
:data => (!User.current.admin? && member.include?(User.current) ? {:confirm => l(:text_own_membership_delete_confirmation)} : {}) if member.deletable? %>
|
| 42 | 56 |
</td> |
| test/functional/members_controller_test.rb | ||
|---|---|---|
| 274 | 274 |
assert_match %r{settings/members\?members_page=}, response.body
|
| 275 | 275 |
end |
| 276 | 276 | |
| 277 |
def test_edit_xhr_should_keep_current_filters_in_form_action |
|
| 278 |
@request.session[:user_id] = 2 |
|
| 279 |
get(:edit, :params => {:id => 2, :member_name => 'Smith', :member_role_id => 1}, :xhr => true)
|
|
| 280 |
assert_response :success |
|
| 281 |
assert_match %r{/memberships/2\?member_name=Smith&member_role_id=1}, response.body
|
|
| 282 |
end |
|
| 283 | ||
| 284 |
def test_new_xhr_should_keep_current_filters_in_form_action |
|
| 285 |
@request.session[:user_id] = 2 |
|
| 286 |
get(:new, :params => {:project_id => 1, :member_name => 'Smith', :member_role_id => 1}, :xhr => true)
|
|
| 287 |
assert_response :success |
|
| 288 |
assert_match %r{/projects/ecookbook/memberships\?member_name=Smith&member_role_id=1}, response.body
|
|
| 289 |
end |
|
| 290 | ||
| 291 |
def test_update_xhr_should_keep_the_members_list_filtered |
|
| 292 |
@request.session[:user_id] = 2 |
|
| 293 |
put( |
|
| 294 |
:update, |
|
| 295 |
:params => {
|
|
| 296 |
:id => 2, |
|
| 297 |
:membership => {:role_ids => [2]},
|
|
| 298 |
:member_role_id => 1 |
|
| 299 |
}, |
|
| 300 |
:xhr => true |
|
| 301 |
) |
|
| 302 |
assert_response :success |
|
| 303 |
# The updated member does not match the filter anymore, but the list |
|
| 304 |
# must still be filtered by role 1 |
|
| 305 |
assert_match %r{/memberships/1\?member_role_id=1}, response.body
|
|
| 306 |
assert_no_match %r{member-4}, response.body
|
|
| 307 |
end |
|
| 308 | ||
| 309 |
def test_destroy_xhr_should_keep_the_members_list_filtered |
|
| 310 |
@request.session[:user_id] = 2 |
|
| 311 |
assert_difference 'Member.count', -1 do |
|
| 312 |
delete(:destroy, :params => {:id => 2, :member_name => 'Smith'}, :xhr => true)
|
|
| 313 |
end |
|
| 314 |
assert_response :success |
|
| 315 |
assert_match %r{/memberships/1\?member_name=Smith}, response.body
|
|
| 316 |
assert_no_match %r{member-4}, response.body
|
|
| 317 |
end |
|
| 318 | ||
| 319 |
def test_destroy_should_redirect_to_the_filtered_members_list |
|
| 320 |
@request.session[:user_id] = 2 |
|
| 321 |
assert_difference 'Member.count', -1 do |
|
| 322 |
delete(:destroy, :params => {:id => 2, :member_name => 'Smith', :member_role_id => 1})
|
|
| 323 |
end |
|
| 324 |
assert_redirected_to '/projects/ecookbook/settings/members?member_name=Smith&member_role_id=1' |
|
| 325 |
end |
|
| 326 | ||
| 277 | 327 |
def test_update_locked_member_should_be_allowed |
| 278 | 328 |
User.find(3).lock! |
| 279 | 329 | |
| test/functional/projects_controller_test.rb | ||
|---|---|---|
| 1112 | 1112 |
@request.session[:user_id] = 1 |
| 1113 | 1113 |
get(:settings, :params => {:id => project.id, :tab => 'members'})
|
| 1114 | 1114 |
assert_response :success |
| 1115 |
assert_select 'div#tab-content-members form#members-filter-form' |
|
| 1115 | 1116 |
assert_select 'div#tab-content-members p.nodata' |
| 1116 | 1117 |
assert_select 'div#tab-content-members table.list.members', :count => 0 |
| 1117 | 1118 |
end |
| 1118 | 1119 | |
| 1120 |
def test_settings_members_should_display_filter_form |
|
| 1121 |
@request.session[:user_id] = 2 |
|
| 1122 |
get(:settings, :params => {:id => 'ecookbook', :tab => 'members'})
|
|
| 1123 |
assert_response :success |
|
| 1124 |
assert_select 'div#tab-content-members form#members-filter-form[action=?]', '/projects/ecookbook/settings/members' do |
|
| 1125 |
assert_select 'input[name=member_name]' |
|
| 1126 |
assert_select 'select[name=member_role_id][onchange=?]', 'this.form.submit(); return false;' |
|
| 1127 |
assert_select 'input[type=submit]' |
|
| 1128 |
assert_select 'a[href=?]', '/projects/ecookbook/settings/members' |
|
| 1129 |
end |
|
| 1130 |
end |
|
| 1131 | ||
| 1132 |
def test_settings_members_should_filter_by_name |
|
| 1133 |
@request.session[:user_id] = 2 |
|
| 1134 |
get( |
|
| 1135 |
:settings, |
|
| 1136 |
:params => {
|
|
| 1137 |
:id => 'ecookbook', |
|
| 1138 |
:tab => 'members', |
|
| 1139 |
:member_name => 'John' |
|
| 1140 |
} |
|
| 1141 |
) |
|
| 1142 |
assert_response :success |
|
| 1143 |
assert_select 'div#tab-content-members form#members-filter-form' do |
|
| 1144 |
assert_select 'input[name=member_name][value=?]', 'John' |
|
| 1145 |
end |
|
| 1146 |
assert_select 'div#tab-content-members tr#member-1' |
|
| 1147 |
assert_select 'div#tab-content-members tr#member-2', :count => 0 |
|
| 1148 |
assert_select 'a#tab-members[href*=?]', 'member_name=John' |
|
| 1149 |
end |
|
| 1150 | ||
| 1151 |
def test_settings_members_should_filter_by_role |
|
| 1152 |
@request.session[:user_id] = 2 |
|
| 1153 |
get( |
|
| 1154 |
:settings, |
|
| 1155 |
:params => {
|
|
| 1156 |
:id => 'ecookbook', |
|
| 1157 |
:tab => 'members', |
|
| 1158 |
:member_role_id => '2' |
|
| 1159 |
} |
|
| 1160 |
) |
|
| 1161 |
assert_response :success |
|
| 1162 |
assert_select 'div#tab-content-members form#members-filter-form' do |
|
| 1163 |
assert_select 'select[name=member_role_id]' do |
|
| 1164 |
assert_select 'option[value="2"][selected=selected]' |
|
| 1165 |
end |
|
| 1166 |
end |
|
| 1167 |
assert_select 'div#tab-content-members tr#member-2' |
|
| 1168 |
assert_select 'div#tab-content-members tr#member-1', :count => 0 |
|
| 1169 |
assert_select 'a#tab-members[href*=?]', 'member_role_id=2' |
|
| 1170 |
end |
|
| 1171 | ||
| 1172 |
def test_settings_members_filter_with_no_matches_should_show_no_data |
|
| 1173 |
@request.session[:user_id] = 2 |
|
| 1174 |
get( |
|
| 1175 |
:settings, |
|
| 1176 |
:params => {
|
|
| 1177 |
:id => 'ecookbook', |
|
| 1178 |
:tab => 'members', |
|
| 1179 |
:member_name => 'NonexistentPrincipal' |
|
| 1180 |
} |
|
| 1181 |
) |
|
| 1182 |
assert_response :success |
|
| 1183 |
assert_select 'div#tab-content-members form#members-filter-form' |
|
| 1184 |
assert_select 'div#tab-content-members p.nodata' |
|
| 1185 |
assert_select 'div#tab-content-members table.list.members', :count => 0 |
|
| 1186 |
end |
|
| 1187 | ||
| 1188 |
def test_settings_members_pagination_should_preserve_filters |
|
| 1189 |
project = Project.find(1) |
|
| 1190 |
per_page = 25 |
|
| 1191 |
(per_page + 5).times do |
|
| 1192 |
user = User.generate! |
|
| 1193 |
Member.create!(:project => project, :principal => user, :role_ids => [2]) |
|
| 1194 |
end |
|
| 1195 |
@request.session[:user_id] = 2 |
|
| 1196 |
with_settings :per_page_options => '25,50,100' do |
|
| 1197 |
get( |
|
| 1198 |
:settings, |
|
| 1199 |
:params => {
|
|
| 1200 |
:id => 'ecookbook', |
|
| 1201 |
:tab => 'members', |
|
| 1202 |
:member_role_id => '2' |
|
| 1203 |
} |
|
| 1204 |
) |
|
| 1205 |
end |
|
| 1206 |
assert_response :success |
|
| 1207 |
assert_select 'div#tab-content-members span.pagination a[href*=?]', 'member_role_id=2' |
|
| 1208 |
end |
|
| 1209 | ||
| 1119 | 1210 |
def test_settings_should_show_tabs_depending_on_permission |
| 1120 | 1211 |
@request.session[:user_id] = 3 |
| 1121 | 1212 |
project = Project.find(1) |
| test/helpers/members_helper_test.rb | ||
|---|---|---|
| 46 | 46 |
project = Project.generate! |
| 47 | 47 |
5.times { User.add_to_project(User.generate!, project) }
|
| 48 | 48 | |
| 49 |
members, member_pages, member_count = paginate_members(project) |
|
| 49 |
members, member_pages, member_count = paginate_members(project.memberships)
|
|
| 50 | 50 | |
| 51 | 51 |
assert_equal 3, members.size |
| 52 | 52 |
assert_equal 3, member_pages.per_page |
| ... | ... | |
| 59 | 59 |
3.times { User.add_to_project(User.generate!, project) }
|
| 60 | 60 |
Member.where(:project_id => project.id).first.update!(:role_ids => [1, 2]) |
| 61 | 61 | |
| 62 |
members, _member_pages, member_count = paginate_members(project) |
|
| 62 |
members, _member_pages, member_count = paginate_members(project.memberships)
|
|
| 63 | 63 | |
| 64 | 64 |
member_ids = members.map(&:id) |
| 65 | 65 |
assert_equal member_ids.uniq, member_ids |
| 66 | 66 |
assert_equal member_count, members.size |
| 67 | 67 |
assert_equal project.memberships.count, member_count |
| 68 | 68 |
end |
| 69 | ||
| 70 |
def test_paginate_members_should_paginate_the_given_scope |
|
| 71 |
stubs(:per_page_option).returns(25) |
|
| 72 |
project = Project.find(1) |
|
| 73 | ||
| 74 |
members, _member_pages, member_count = paginate_members(project.memberships.with_role(2)) |
|
| 75 | ||
| 76 |
assert_equal [2, 4], members.map(&:id).sort |
|
| 77 |
assert_equal 2, member_count |
|
| 78 |
end |
|
| 69 | 79 |
end |
| test/unit/member_test.rb | ||
|---|---|---|
| 33 | 33 |
assert_equal roles, roles.sort |
| 34 | 34 |
end |
| 35 | 35 | |
| 36 |
def test_like_scope_should_match_users_and_groups |
|
| 37 |
assert_equal [1], Project.find(1).memberships.like('Smith').ids
|
|
| 38 |
assert_equal [9], Project.find(2).memberships.like('B Team').ids
|
|
| 39 |
end |
|
| 40 | ||
| 41 |
def test_like_scope_with_blank_value_should_return_all_the_members |
|
| 42 |
project = Project.find(1) |
|
| 43 |
assert_equal project.memberships.ids.sort, project.memberships.like('').ids.sort
|
|
| 44 |
end |
|
| 45 | ||
| 46 |
def test_with_role_scope_should_return_the_members_having_the_role |
|
| 47 |
assert_equal [2, 4], Project.find(1).memberships.with_role(2).ids.sort |
|
| 48 |
end |
|
| 49 | ||
| 50 |
def test_with_role_scope_should_return_the_members_having_the_role_inherited |
|
| 51 |
# Member 7 has the role 1 inherited from the member 6 |
|
| 52 |
assert_include 7, Project.find(5).memberships.with_role(1).ids |
|
| 53 |
end |
|
| 54 | ||
| 55 |
def test_with_role_scope_with_blank_value_should_return_all_the_members |
|
| 56 |
project = Project.find(1) |
|
| 57 |
assert_equal project.memberships.ids.sort, project.memberships.with_role(nil).ids.sort |
|
| 58 |
end |
|
| 59 | ||
| 36 | 60 |
def test_create |
| 37 | 61 |
member = Member.new(:project_id => 1, :user_id => 4, :role_ids => [1, 2]) |
| 38 | 62 |
assert member.save |
- « Previous
- 1
- 2
- Next »